mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-09 22:00:18 -04:00
Compare commits
12 Commits
be87cd13cf
...
2085e45b3e
Author | SHA1 | Date | |
---|---|---|---|
|
2085e45b3e | ||
|
55a03ff3fd | ||
|
0daf80d222 | ||
|
f6b0838c99 | ||
|
dff05af79c | ||
|
251222167d | ||
|
a516c414f6 | ||
|
fbde92a524 | ||
|
00ba6d7df1 | ||
|
fdbf4ffa40 | ||
|
51c40684fa | ||
|
65c6da5b96 |
|
@ -44,7 +44,7 @@ RUN mkdir -p /etc/apt/keyrings \
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list \
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install nodejs -y \
|
&& apt-get install nodejs -y \
|
||||||
&& npm install -g npm@latest \
|
&& npm install -g npm@10.8.2 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user
|
# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
/backend/.env
|
/backend/.env
|
||||||
__pycache__
|
__pycache__
|
||||||
node_modules
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
|
|
@ -19,7 +19,10 @@ openapi_tags = {
|
||||||
# TODO: Create custom exceptions
|
# TODO: Create custom exceptions
|
||||||
@api.post("", response_model=Resource, tags=["Resource"])
|
@api.post("", response_model=Resource, tags=["Resource"])
|
||||||
def create(
|
def create(
|
||||||
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
|
uuid: str,
|
||||||
|
resource: Resource,
|
||||||
|
user_svc: UserService = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
subject = user_svc.get_user_by_uuid(uuid)
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
return resource_svc.create(subject, resource)
|
return resource_svc.create(subject, resource)
|
||||||
|
@ -27,14 +30,20 @@ def create(
|
||||||
|
|
||||||
@api.get("", response_model=List[Resource], tags=["Resource"])
|
@api.get("", response_model=List[Resource], tags=["Resource"])
|
||||||
def get_all(
|
def get_all(
|
||||||
uuid: str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
|
uuid: str,
|
||||||
|
user_svc: UserService = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
subject = user_svc.get_user_by_uuid(uuid)
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
return resource_svc.get_resource_by_user(subject)
|
return resource_svc.get_resource_by_user(subject)
|
||||||
|
|
||||||
|
|
||||||
@api.get("/{name}", response_model=Resource, tags=["Resource"])
|
@api.get("/{name}", response_model=Resource, tags=["Resource"])
|
||||||
def get_by_name(
|
def get_by_name(
|
||||||
name:str, uuid:str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
|
name: str,
|
||||||
|
uuid: str,
|
||||||
|
user_svc: UserService = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
subject = user_svc.get_user_by_uuid(uuid)
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
return resource_svc.get_resource_by_name(name, subject)
|
return resource_svc.get_resource_by_name(name, subject)
|
||||||
|
@ -42,7 +51,10 @@ def get_by_name(
|
||||||
|
|
||||||
@api.put("", response_model=Resource, tags=["Resource"])
|
@api.put("", response_model=Resource, tags=["Resource"])
|
||||||
def update(
|
def update(
|
||||||
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
|
uuid: str,
|
||||||
|
resource: Resource,
|
||||||
|
user_svc: UserService = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
subject = user_svc.get_user_by_uuid(uuid)
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
return resource_svc.update(subject, resource)
|
return resource_svc.update(subject, resource)
|
||||||
|
@ -50,7 +62,10 @@ def update(
|
||||||
|
|
||||||
@api.delete("", response_model=None, tags=["Resource"])
|
@api.delete("", response_model=None, tags=["Resource"])
|
||||||
def delete(
|
def delete(
|
||||||
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
|
uuid: str,
|
||||||
|
resource: Resource,
|
||||||
|
user_svc: UserService = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
subject = user_svc.get_user_by_uuid(uuid)
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
resource_svc.delete(subject, resource)
|
resource_svc.delete(subject, resource)
|
||||||
|
|
|
@ -16,8 +16,8 @@ openapi_tags = {
|
||||||
# TODO: Enable authorization by passing user uuid to API
|
# TODO: Enable authorization by passing user uuid to API
|
||||||
# TODO: Create custom exceptions
|
# TODO: Create custom exceptions
|
||||||
@api.get("/all", response_model=List[User], tags=["Users"])
|
@api.get("/all", response_model=List[User], tags=["Users"])
|
||||||
def get_all(user_id: str, user_svc: UserService = Depends()):
|
def get_all(uuid: str, user_svc: UserService = Depends()):
|
||||||
subject = user_svc.get_user_by_uuid(user_id)
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
|
|
||||||
if subject.role != UserTypeEnum.ADMIN:
|
if subject.role != UserTypeEnum.ADMIN:
|
||||||
raise Exception(f"Insufficient permissions for user {subject.uuid}")
|
raise Exception(f"Insufficient permissions for user {subject.uuid}")
|
||||||
|
@ -25,6 +25,24 @@ def get_all(user_id: str, user_svc: UserService = Depends()):
|
||||||
return user_svc.all()
|
return user_svc.all()
|
||||||
|
|
||||||
|
|
||||||
@api.get("/{user_id}", response_model=User, tags=["Users"])
|
@api.get("/{uuid}", response_model=User, tags=["Users"])
|
||||||
def get_by_uuid(user_id: str, user_svc: UserService = Depends()):
|
def get_by_uuid(uuid: str, user_svc: UserService = Depends()):
|
||||||
return user_svc.get_user_by_uuid(user_id)
|
return user_svc.get_user_by_uuid(uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/", response_model=User, tags=["Users"])
|
||||||
|
def create_user(uuid: str, user: User, user_svc: UserService = Depends()):
|
||||||
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
|
if subject.role != UserTypeEnum.ADMIN:
|
||||||
|
raise Exception(f"Insufficient permissions for user {subject.uuid}")
|
||||||
|
|
||||||
|
return user_svc.create(user)
|
||||||
|
|
||||||
|
|
||||||
|
@api.put("/", response_model=User, tags=["Users"])
|
||||||
|
def update_user(uuid: str, user: User, user_svc: UserService = Depends()):
|
||||||
|
subject = user_svc.get_user_by_uuid(uuid)
|
||||||
|
if subject.role != UserTypeEnum.ADMIN:
|
||||||
|
raise Exception(f"Insufficient permissions for user {subject.uuid}")
|
||||||
|
|
||||||
|
return user_svc.update(user)
|
||||||
|
|
|
@ -38,8 +38,8 @@ class UserEntity(EntityBase):
|
||||||
program: Mapped[list[ProgramTypeEnum]] = mapped_column(
|
program: Mapped[list[ProgramTypeEnum]] = mapped_column(
|
||||||
ARRAY(Enum(ProgramTypeEnum)), nullable=False
|
ARRAY(Enum(ProgramTypeEnum)), nullable=False
|
||||||
)
|
)
|
||||||
experience: Mapped[int] = mapped_column(Integer, nullable=False)
|
experience: Mapped[int] = mapped_column(Integer, nullable=True)
|
||||||
group: Mapped[str] = mapped_column(String(50))
|
group: Mapped[str] = mapped_column(String(50), nullable=True)
|
||||||
uuid: Mapped[str] = mapped_column(String, nullable=True)
|
uuid: Mapped[str] = mapped_column(String, nullable=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -10,9 +10,9 @@ class User(BaseModel):
|
||||||
id: int | None = None
|
id: int | None = None
|
||||||
username: str = Field(..., description="The username of the user")
|
username: str = Field(..., description="The username of the user")
|
||||||
email: str = Field(..., description="The e-mail of the user")
|
email: str = Field(..., description="The e-mail of the user")
|
||||||
experience: int = Field(..., description="Years of Experience of the User")
|
experience: int | None = Field(None, description="Years of Experience of the User")
|
||||||
group: str
|
group: str | None = Field(None, description="The group of the user")
|
||||||
program: List[ProgramTypeEnum]
|
program: List[ProgramTypeEnum] | None = None
|
||||||
role: UserTypeEnum
|
role: UserTypeEnum | None = None
|
||||||
created_at: Optional[datetime] = datetime.now()
|
created_at: Optional[datetime] = datetime.now()
|
||||||
uuid: str | None = None
|
uuid: str | None = None
|
||||||
|
|
|
@ -17,7 +17,7 @@ class ResourceService:
|
||||||
def get_resource_by_user(self, subject: User) -> list[Resource]:
|
def get_resource_by_user(self, subject: User) -> list[Resource]:
|
||||||
"""Resource method getting all of the resources that a user has access to based on role"""
|
"""Resource method getting all of the resources that a user has access to based on role"""
|
||||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||||
query = select(ResourceEntity)
|
query = select(ResourceEntity).order_by(ResourceEntity.id)
|
||||||
entities = self._session.scalars(query).all()
|
entities = self._session.scalars(query).all()
|
||||||
return [resource.to_model() for resource in entities]
|
return [resource.to_model() for resource in entities]
|
||||||
else:
|
else:
|
||||||
|
@ -86,10 +86,10 @@ class ResourceService:
|
||||||
raise ResourceNotFoundException(
|
raise ResourceNotFoundException(
|
||||||
f"No resource found with matching id: {resource.id}"
|
f"No resource found with matching id: {resource.id}"
|
||||||
)
|
)
|
||||||
entity.name = resource.name
|
entity.name = resource.name if resource.name else entity.name
|
||||||
entity.summary = resource.summary
|
entity.summary = resource.summary if resource.summary else entity.summary
|
||||||
entity.link = resource.link
|
entity.link = resource.link if resource.link else entity.link
|
||||||
entity.program = resource.program
|
entity.program = resource.program if resource.program else entity.program
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
return entity.to_model()
|
return entity.to_model()
|
||||||
|
|
||||||
|
|
|
@ -22,14 +22,18 @@ class ServiceService:
|
||||||
def get_service_by_user(self, subject: User) -> list[Service]:
|
def get_service_by_user(self, subject: User) -> list[Service]:
|
||||||
"""Resource method getting all of the resources that a user has access to based on role"""
|
"""Resource method getting all of the resources that a user has access to based on role"""
|
||||||
if subject.role != UserTypeEnum.VOLUNTEER:
|
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||||
query = select(ServiceEntity)
|
query = select(ServiceEntity).order_by(ServiceEntity.id)
|
||||||
entities = self._session.scalars(query).all()
|
entities = self._session.scalars(query).all()
|
||||||
return [service.to_model() for service in entities]
|
return [service.to_model() for service in entities]
|
||||||
else:
|
else:
|
||||||
programs = subject.program
|
programs = subject.program
|
||||||
services = []
|
services = []
|
||||||
for program in programs:
|
for program in programs:
|
||||||
entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all()
|
entities = (
|
||||||
|
self._session.query(ServiceEntity)
|
||||||
|
.where(ServiceEntity.program == program)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
services.append(entity.to_model())
|
services.append(entity.to_model())
|
||||||
return [service for service in services]
|
return [service for service in services]
|
||||||
|
@ -37,14 +41,14 @@ class ServiceService:
|
||||||
def get_service_by_name(self, name: str, subject: User) -> Service:
|
def get_service_by_name(self, name: str, subject: User) -> Service:
|
||||||
"""Service method getting services by id."""
|
"""Service method getting services by id."""
|
||||||
query = select(ServiceEntity).where(
|
query = select(ServiceEntity).where(
|
||||||
and_(
|
and_(ServiceEntity.name == name, ServiceEntity.program.in_(subject.program))
|
||||||
ServiceEntity.name == name, ServiceEntity.program.in_(subject.program)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
entity = self._session.scalars(query).one_or_none()
|
entity = self._session.scalars(query).one_or_none()
|
||||||
|
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned")
|
raise ServiceNotFoundException(
|
||||||
|
f"Service with name: {name} does not exist or program has not been assigned"
|
||||||
|
)
|
||||||
|
|
||||||
return entity.to_model()
|
return entity.to_model()
|
||||||
|
|
||||||
|
@ -66,7 +70,7 @@ class ServiceService:
|
||||||
raise ProgramNotAssignedException(
|
raise ProgramNotAssignedException(
|
||||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
||||||
)
|
)
|
||||||
|
|
||||||
query = select(ServiceEntity).where(ServiceEntity.id == service.id)
|
query = select(ServiceEntity).where(ServiceEntity.id == service.id)
|
||||||
entity = self._session.scalars(query).one_or_none()
|
entity = self._session.scalars(query).one_or_none()
|
||||||
|
|
||||||
|
@ -75,11 +79,13 @@ class ServiceService:
|
||||||
"The service you are searching for does not exist."
|
"The service you are searching for does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
entity.name = service.name
|
entity.name = service.name if service.name else entity.name
|
||||||
entity.status = service.status
|
entity.status = service.status if service.status else entity.status
|
||||||
entity.summary = service.summary
|
entity.summary = service.summary if service.summary else entity.summary
|
||||||
entity.requirements = service.requirements
|
entity.requirements = (
|
||||||
entity.program = service.program
|
service.requirements if service.requirements else entity.requirements
|
||||||
|
)
|
||||||
|
entity.program = service.program if service.program else entity.program
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
|
|
||||||
return entity.to_model()
|
return entity.to_model()
|
||||||
|
@ -88,7 +94,7 @@ class ServiceService:
|
||||||
"""Deletes a service from the table."""
|
"""Deletes a service from the table."""
|
||||||
if subject.role != UserTypeEnum.ADMIN:
|
if subject.role != UserTypeEnum.ADMIN:
|
||||||
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
|
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
|
||||||
|
|
||||||
query = select(ServiceEntity).where(ServiceEntity.id == service.id)
|
query = select(ServiceEntity).where(ServiceEntity.id == service.id)
|
||||||
entity = self._session.scalars(query).one_or_none()
|
entity = self._session.scalars(query).one_or_none()
|
||||||
|
|
||||||
|
|
|
@ -43,10 +43,10 @@ class UserService:
|
||||||
|
|
||||||
def all(self) -> list[User]:
|
def all(self) -> list[User]:
|
||||||
"""
|
"""
|
||||||
Returns a list of all Users
|
Returns a list of all Users ordered by id
|
||||||
|
|
||||||
"""
|
"""
|
||||||
query = select(UserEntity)
|
query = select(UserEntity).order_by(UserEntity.id)
|
||||||
entities = self._session.scalars(query).all()
|
entities = self._session.scalars(query).all()
|
||||||
|
|
||||||
return [entity.to_model() for entity in entities]
|
return [entity.to_model() for entity in entities]
|
||||||
|
@ -61,22 +61,21 @@ class UserService:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if (user.id != None):
|
if user.id != None:
|
||||||
user = self.get_user_by_id(user.id)
|
user = self.get_user_by_id(user.id)
|
||||||
|
else:
|
||||||
|
user_entity = UserEntity.from_model(user)
|
||||||
|
# add new user to table
|
||||||
|
self._session.add(user_entity)
|
||||||
|
self._session.commit()
|
||||||
except:
|
except:
|
||||||
# if does not exist, create new object
|
raise Exception(f"Failed to create user")
|
||||||
user_entity = UserEntity.from_model(user)
|
|
||||||
|
|
||||||
# add new user to table
|
return user
|
||||||
self._session.add(user_entity)
|
|
||||||
self._session.commit()
|
def delete(self, user: User) -> None:
|
||||||
finally:
|
|
||||||
# return added object
|
|
||||||
return user
|
|
||||||
|
|
||||||
def delete(self, user: User) -> None:
|
|
||||||
"""
|
"""
|
||||||
Delete a user
|
Delete a user
|
||||||
|
|
||||||
Args: the user to delete
|
Args: the user to delete
|
||||||
|
|
||||||
|
@ -86,34 +85,30 @@ class UserService:
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
raise Exception(f"No matching user found")
|
raise Exception(f"No matching user found")
|
||||||
|
|
||||||
self._session.delete(obj)
|
self._session.delete(obj)
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
|
|
||||||
|
def update(self, user: User) -> User:
|
||||||
|
|
||||||
def update(self, user: User) -> User:
|
|
||||||
"""
|
"""
|
||||||
Updates a user
|
Updates a user
|
||||||
|
|
||||||
Args: User to be updated
|
Args: User to be updated
|
||||||
|
|
||||||
Returns: The updated User
|
Returns: The updated User
|
||||||
"""
|
"""
|
||||||
obj = self._session.get(UserEntity, user.id)
|
obj = self._session.get(UserEntity, user.id)
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
raise Exception(f"No matching user found")
|
raise Exception(f"No matching user found")
|
||||||
|
|
||||||
obj.username = user.username
|
obj.username = user.username if user.username else obj.username
|
||||||
obj.role = user.role
|
obj.role = user.role if user.role else obj.role
|
||||||
obj.email = user.email
|
obj.email = user.email if user.email else obj.email
|
||||||
obj.program = user.program
|
obj.program = user.program if user.program else obj.program
|
||||||
obj.experience = user.experience
|
obj.experience = user.experience if user.experience else obj.experience
|
||||||
obj.group = user.group
|
obj.group = user.group if user.group else obj.group
|
||||||
|
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
|
|
||||||
return obj.to_model()
|
return obj.to_model()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
|
|
@ -4,41 +4,98 @@ import { PageLayout } from "@/components/PageLayout";
|
||||||
import UserTable from "@/components/Table/UserTable";
|
import UserTable from "@/components/Table/UserTable";
|
||||||
import User from "@/utils/models/User";
|
import User from "@/utils/models/User";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
|
||||||
import { UsersIcon } from "@heroicons/react/24/solid";
|
import { UsersIcon } from "@heroicons/react/24/solid";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [currUser, setCurrUser] = useState<User | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getUser() {
|
async function getUsers() {
|
||||||
const supabase = createClient();
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.getUser();
|
const supabase = createClient();
|
||||||
|
const { data: userData, error: authError } =
|
||||||
|
await supabase.auth.getUser();
|
||||||
|
|
||||||
if (error) {
|
if (authError) {
|
||||||
console.log("Accessed admin page but not logged in");
|
throw new Error("Authentication failed. Please sign in.");
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
// Fetch users list and current user data in parallel
|
||||||
|
const [usersResponse, userResponse] = await Promise.all([
|
||||||
|
fetch(`/api/user/all?uuid=${userData.user.id}`),
|
||||||
|
fetch(`/api/user?uuid=${userData.user.id}`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check for HTTP errors
|
||||||
|
if (!usersResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch users: ${usersResponse.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!userResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch user data: ${userResponse.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the responses
|
||||||
|
const [usersAPI, currUserData] = await Promise.all([
|
||||||
|
usersResponse.json(),
|
||||||
|
userResponse.json(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify admin status
|
||||||
|
if (currUserData.role !== "ADMIN") {
|
||||||
|
throw new Error("Unauthorized: Admin access required");
|
||||||
|
}
|
||||||
|
|
||||||
|
setUsers(usersAPI);
|
||||||
|
setCurrUser(currUserData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching data:", err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: "An unexpected error occurred"
|
||||||
|
);
|
||||||
|
setUsers([]);
|
||||||
|
setCurrUser(undefined);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userListData = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const users: User[] = await userListData.json();
|
|
||||||
|
|
||||||
setUsers(users);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser();
|
getUsers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* icon + title */}
|
|
||||||
<PageLayout title="Users" icon={<UsersIcon />}>
|
<PageLayout title="Users" icon={<UsersIcon />}>
|
||||||
<UserTable data={users} setData={setUsers} />
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-purple-700" />
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-red-500 text-center">
|
||||||
|
<p className="text-lg font-semibold">Error</p>
|
||||||
|
<p className="text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<UserTable
|
||||||
|
data={users}
|
||||||
|
setData={setUsers}
|
||||||
|
user={currUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
37
compass/app/api/resource/create/route.ts
Normal file
37
compass/app/api/resource/create/route.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import Resource from "@/utils/models/Resource";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resourceData = await request.json();
|
||||||
|
|
||||||
|
console.log("RESOURCE DATA", JSON.stringify(resourceData));
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
|
// Send the POST request to the backend
|
||||||
|
const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(resourceData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdResource: Resource = await response.json();
|
||||||
|
return NextResponse.json(createdResource, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating resource:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create resource" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
compass/app/api/resource/update/route.ts
Normal file
37
compass/app/api/resource/update/route.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import Resource from "@/utils/models/Resource";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resourceData = await request.json();
|
||||||
|
|
||||||
|
console.log("RESOURCE DATA", JSON.stringify(resourceData));
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
|
// Send the POST request to the backend
|
||||||
|
const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(resourceData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource: Resource = await response.json();
|
||||||
|
return NextResponse.json(resource, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating user:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to update resource" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
compass/app/api/service/create/route.ts
Normal file
37
compass/app/api/service/create/route.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serviceData = await request.json();
|
||||||
|
|
||||||
|
console.log("SERVICE DATA", JSON.stringify(serviceData));
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
|
// Send the POST request to the backend
|
||||||
|
const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(serviceData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdService: User = await response.json();
|
||||||
|
return NextResponse.json(createdService, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating service:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create service" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
compass/app/api/service/update/route.ts
Normal file
37
compass/app/api/service/update/route.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import Service from "@/utils/models/Service";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serviceData = await request.json();
|
||||||
|
|
||||||
|
console.log("SERVICE DATA", JSON.stringify(serviceData));
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
|
// Send the POST request to the backend
|
||||||
|
const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(serviceData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdService: Service = await response.json();
|
||||||
|
return NextResponse.json(createdService, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating service:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to update service" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const uuid = searchParams.get("uuid");
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
const data = await fetch(`${apiEndpoint}?uuid=${uuid}`);
|
||||||
|
|
||||||
const userData: User[] = await data.json();
|
const userData: User[] = await data.json();
|
||||||
// TODO: Remove make every user visible
|
// TODO: Remove make every user visible
|
||||||
|
|
37
compass/app/api/user/create/route.ts
Normal file
37
compass/app/api/user/create/route.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = await request.json();
|
||||||
|
|
||||||
|
console.log("USER DATA", JSON.stringify(userData));
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
|
// Send the POST request to the backend
|
||||||
|
const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(userData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdUser: User = await response.json();
|
||||||
|
return NextResponse.json(createdUser, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating user:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create user" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
compass/app/api/user/update/route.ts
Normal file
37
compass/app/api/user/update/route.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = await request.json();
|
||||||
|
|
||||||
|
console.log("USER DATA", JSON.stringify(userData));
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
|
// Send the POST request to the backend
|
||||||
|
const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(userData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdUser: User = await response.json();
|
||||||
|
return NextResponse.json(createdUser, { status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating user:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create user" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,8 @@ export default function Page() {
|
||||||
alt="Compass Center logo."
|
alt="Compass Center logo."
|
||||||
width={100}
|
width={100}
|
||||||
height={91}
|
height={91}
|
||||||
|
style={{ height: "auto", width: "auto" }}
|
||||||
|
priority
|
||||||
/>
|
/>
|
||||||
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
|
@ -4,31 +4,68 @@ import { PageLayout } from "@/components/PageLayout";
|
||||||
import Resource from "@/utils/models/Resource";
|
import Resource from "@/utils/models/Resource";
|
||||||
import ResourceTable from "@/components/Table/ResourceTable";
|
import ResourceTable from "@/components/Table/ResourceTable";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
|
||||||
import { BookmarkIcon } from "@heroicons/react/24/solid";
|
import { BookmarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [resources, setResources] = useState<Resource[]>([]);
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
|
const [currUser, setCurrUser] = useState<User | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getResources() {
|
async function getResources() {
|
||||||
const supabase = createClient();
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.getUser();
|
const supabase = createClient();
|
||||||
|
const { data: userData, error: authError } =
|
||||||
|
await supabase.auth.getUser();
|
||||||
|
|
||||||
if (error) {
|
if (authError) {
|
||||||
console.log("Accessed admin page but not logged in");
|
throw new Error("Authentication failed. Please sign in.");
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
// Fetch resources and user data in parallel
|
||||||
|
const [resourceResponse, userResponse] = await Promise.all([
|
||||||
|
fetch(`/api/resource/all?uuid=${userData.user.id}`),
|
||||||
|
fetch(`/api/user?uuid=${userData.user.id}`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check for HTTP errors
|
||||||
|
if (!resourceResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch resources: ${resourceResponse.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!userResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch user data: ${userResponse.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the responses
|
||||||
|
const [resourcesAPI, currUserData] = await Promise.all([
|
||||||
|
resourceResponse.json(),
|
||||||
|
userResponse.json(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setResources(resourcesAPI);
|
||||||
|
setCurrUser(currUserData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching data:", err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: "An unexpected error occurred"
|
||||||
|
);
|
||||||
|
setResources([]);
|
||||||
|
setCurrUser(undefined);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userListData = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_HOST}/api/resource/all?uuid=${data.user.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const resourcesAPI: Resource[] = await userListData.json();
|
|
||||||
|
|
||||||
setResources(resourcesAPI);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getResources();
|
getResources();
|
||||||
|
@ -36,9 +73,25 @@ export default function Page() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* icon + title */}
|
|
||||||
<PageLayout title="Resources" icon={<BookmarkIcon />}>
|
<PageLayout title="Resources" icon={<BookmarkIcon />}>
|
||||||
<ResourceTable data={resources} setData={setResources} />
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-purple-700" />
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-red-500 text-center">
|
||||||
|
<p className="text-lg font-semibold">Error</p>
|
||||||
|
<p className="text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ResourceTable
|
||||||
|
data={resources}
|
||||||
|
setData={setResources}
|
||||||
|
user={currUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,31 +3,69 @@
|
||||||
import { PageLayout } from "@/components/PageLayout";
|
import { PageLayout } from "@/components/PageLayout";
|
||||||
import ServiceTable from "@/components/Table/ServiceTable";
|
import ServiceTable from "@/components/Table/ServiceTable";
|
||||||
import Service from "@/utils/models/Service";
|
import Service from "@/utils/models/Service";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
|
||||||
import { ClipboardIcon } from "@heroicons/react/24/solid";
|
import { ClipboardIcon } from "@heroicons/react/24/solid";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [services, setServices] = useState<Service[]>([]);
|
const [services, setServices] = useState<Service[]>([]);
|
||||||
|
const [currUser, setCurrUser] = useState<User | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getServices() {
|
async function getServices() {
|
||||||
const supabase = createClient();
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.getUser();
|
const supabase = createClient();
|
||||||
|
const { data: userData, error: authError } =
|
||||||
|
await supabase.auth.getUser();
|
||||||
|
|
||||||
if (error) {
|
if (authError) {
|
||||||
console.log("Accessed admin page but not logged in");
|
throw new Error("Authentication failed. Please sign in.");
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
// Fetch services and user data in parallel
|
||||||
|
const [serviceResponse, userResponse] = await Promise.all([
|
||||||
|
fetch(`/api/service/all?uuid=${userData.user.id}`),
|
||||||
|
fetch(`/api/user?uuid=${userData.user.id}`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check for HTTP errors
|
||||||
|
if (!serviceResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch services: ${serviceResponse.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!userResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch user data: ${userResponse.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the responses
|
||||||
|
const [servicesAPI, currUserData] = await Promise.all([
|
||||||
|
serviceResponse.json(),
|
||||||
|
userResponse.json(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setCurrUser(currUserData);
|
||||||
|
setServices(servicesAPI);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching data:", err);
|
||||||
|
setError(
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: "An unexpected error occurred"
|
||||||
|
);
|
||||||
|
setServices([]);
|
||||||
|
setCurrUser(undefined);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceListData = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.user.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const servicesAPI: Service[] = await serviceListData.json();
|
|
||||||
setServices(servicesAPI);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getServices();
|
getServices();
|
||||||
|
@ -35,9 +73,25 @@ export default function Page() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* icon + title */}
|
|
||||||
<PageLayout title="Services" icon={<ClipboardIcon />}>
|
<PageLayout title="Services" icon={<ClipboardIcon />}>
|
||||||
<ServiceTable data={services} setData={setServices} />
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-purple-700" />
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="text-red-500 text-center">
|
||||||
|
<p className="text-lg font-semibold">Error</p>
|
||||||
|
<p className="text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ServiceTable
|
||||||
|
data={services}
|
||||||
|
setData={setServices}
|
||||||
|
user={currUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -76,6 +76,8 @@ export default function Page() {
|
||||||
alt="Compass Center logo."
|
alt="Compass Center logo."
|
||||||
width={100}
|
width={100}
|
||||||
height={91}
|
height={91}
|
||||||
|
style={{ height: "auto", width: "auto" }}
|
||||||
|
priority
|
||||||
/>
|
/>
|
||||||
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
<h1 className="font-bold text-2xl text-purple-800">Login</h1>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
232
compass/components/Drawer/CreateDrawer.tsx
Normal file
232
compass/components/Drawer/CreateDrawer.tsx
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
||||||
|
import {
|
||||||
|
ArrowsPointingOutIcon,
|
||||||
|
ArrowsPointingInIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import TagsInput from "../TagsInput/Index";
|
||||||
|
import { Details } from "./Drawer";
|
||||||
|
|
||||||
|
type CreateDrawerProps = {
|
||||||
|
details: Details[];
|
||||||
|
onCreate: (newItem: any) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateDrawer: FunctionComponent<CreateDrawerProps> = ({
|
||||||
|
details,
|
||||||
|
onCreate,
|
||||||
|
}: CreateDrawerProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isFull, setIsFull] = useState(false);
|
||||||
|
const [newItemContent, setNewItemContent] = useState<any>({});
|
||||||
|
const [renderKey, setRenderKey] = useState(0);
|
||||||
|
|
||||||
|
const handleContentChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setNewItemContent((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeSelectField = (key: string) => {
|
||||||
|
if (!newItemContent[key]) {
|
||||||
|
setNewItemContent((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (onCreate(newItemContent)) {
|
||||||
|
console.log("newItemContent", newItemContent);
|
||||||
|
setNewItemContent({});
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDrawer = () => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
if (isFull) {
|
||||||
|
setIsFull(!isFull);
|
||||||
|
}
|
||||||
|
if (!isOpen) {
|
||||||
|
setRenderKey((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDrawerFullScreen = () => setIsFull(!isFull);
|
||||||
|
|
||||||
|
const drawerClassName = `fixed top-0 right-0 h-full bg-white transform ease-in-out duration-300 z-20 overflow-y-auto ${
|
||||||
|
isOpen ? "translate-x-0 shadow-2xl" : "translate-x-full"
|
||||||
|
} ${isFull ? "w-full" : "w-[600px]"}`;
|
||||||
|
|
||||||
|
const iconComponent = isFull ? (
|
||||||
|
<ArrowsPointingInIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="text-sm text-white font-medium bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded-md"
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
>
|
||||||
|
Create New
|
||||||
|
</button>
|
||||||
|
<div className={drawerClassName}>
|
||||||
|
<div className="sticky top-0 flex items-center justify-between p-4 bg-white border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800">
|
||||||
|
Create New Item
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleDrawerFullScreen}
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg"
|
||||||
|
>
|
||||||
|
{iconComponent}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg"
|
||||||
|
>
|
||||||
|
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
{details.map((detail, index) => {
|
||||||
|
const value = newItemContent[detail.key] || "";
|
||||||
|
let inputField;
|
||||||
|
|
||||||
|
switch (detail.inputType) {
|
||||||
|
case "select-one":
|
||||||
|
initializeSelectField(detail.key);
|
||||||
|
inputField = (
|
||||||
|
<TagsInput
|
||||||
|
key={`${detail.key}-${renderKey}`}
|
||||||
|
presetValue={[]}
|
||||||
|
presetOptions={
|
||||||
|
detail.presetOptionsValues || []
|
||||||
|
}
|
||||||
|
setPresetOptions={
|
||||||
|
detail.presetOptionsSetter ||
|
||||||
|
(() => {})
|
||||||
|
}
|
||||||
|
singleValue={true}
|
||||||
|
onTagsChange={(
|
||||||
|
tags: Set<string>
|
||||||
|
) => {
|
||||||
|
setNewItemContent(
|
||||||
|
(prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
[detail.key]:
|
||||||
|
tags.size > 0
|
||||||
|
? Array.from(
|
||||||
|
tags
|
||||||
|
)[0]
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "select-multiple":
|
||||||
|
initializeSelectField(detail.key);
|
||||||
|
inputField = (
|
||||||
|
<TagsInput
|
||||||
|
key={`${detail.key}-${renderKey}`}
|
||||||
|
presetValue={
|
||||||
|
newItemContent[detail.key] || []
|
||||||
|
}
|
||||||
|
presetOptions={
|
||||||
|
detail.presetOptionsValues || []
|
||||||
|
}
|
||||||
|
setPresetOptions={
|
||||||
|
detail.presetOptionsSetter ||
|
||||||
|
(() => {})
|
||||||
|
}
|
||||||
|
onTagsChange={(
|
||||||
|
tags: Set<string>
|
||||||
|
) => {
|
||||||
|
setNewItemContent(
|
||||||
|
(prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
[detail.key]:
|
||||||
|
Array.from(tags),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "textarea":
|
||||||
|
inputField = (
|
||||||
|
<textarea
|
||||||
|
name={detail.key}
|
||||||
|
value={value}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
rows={4}
|
||||||
|
onInput={(e) => {
|
||||||
|
const target =
|
||||||
|
e.target as HTMLTextAreaElement;
|
||||||
|
target.style.height = "auto";
|
||||||
|
target.style.height =
|
||||||
|
target.scrollHeight + "px";
|
||||||
|
}}
|
||||||
|
className="w-full p-2 focus:outline-none border border-gray-200 rounded-md resize-none font-normal"
|
||||||
|
placeholder={`Enter ${detail.label.toLowerCase()}...`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
inputField = (
|
||||||
|
<input
|
||||||
|
type={detail.inputType}
|
||||||
|
name={detail.key}
|
||||||
|
value={value}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
className="w-full p-2 border border-gray-200 rounded-md focus:outline-none focus:border-purple-500"
|
||||||
|
placeholder={`Enter ${detail.label.toLowerCase()}...`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<label className="flex items-center text-sm text-gray-700 gap-2">
|
||||||
|
<span className="text-gray-500">
|
||||||
|
{detail.icon}
|
||||||
|
</span>
|
||||||
|
{detail.label}
|
||||||
|
</label>
|
||||||
|
{inputField}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateDrawer;
|
|
@ -1,78 +1,109 @@
|
||||||
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
|
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
||||||
import {
|
import { StarIcon as SolidStarIcon, UserIcon } from "@heroicons/react/24/solid";
|
||||||
StarIcon as SolidStarIcon,
|
|
||||||
EnvelopeIcon,
|
|
||||||
UserIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
import {
|
import {
|
||||||
ArrowsPointingOutIcon,
|
ArrowsPointingOutIcon,
|
||||||
ArrowsPointingInIcon,
|
ArrowsPointingInIcon,
|
||||||
StarIcon as OutlineStarIcon,
|
StarIcon as OutlineStarIcon,
|
||||||
ListBulletIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import TagsInput from "../TagsInput/Index";
|
import TagsInput from "../TagsInput/Index";
|
||||||
|
import { Tag } from "../TagsInput/Tag";
|
||||||
|
|
||||||
type DrawerProps = {
|
type InputType =
|
||||||
title: string;
|
| "text"
|
||||||
children: ReactNode;
|
| "email"
|
||||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
| "textarea"
|
||||||
type?: "button" | "submit" | "reset"; // specify possible values for type
|
| "select-one"
|
||||||
disabled?: boolean;
|
| "select-multiple";
|
||||||
editableContent?: any;
|
|
||||||
onSave?: (content: any) => void;
|
|
||||||
rowContent?: any;
|
|
||||||
setData: Dispatch<SetStateAction<any>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EditContent {
|
export interface Details {
|
||||||
content: string;
|
key: string;
|
||||||
isEditing: boolean;
|
label: string;
|
||||||
|
inputType: InputType;
|
||||||
|
icon: ReactNode;
|
||||||
|
presetOptionsValues?: string[];
|
||||||
|
presetOptionsSetter?: Dispatch<SetStateAction<string[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DrawerProps = {
|
||||||
|
titleKey: string;
|
||||||
|
details: Details[];
|
||||||
|
rowContent?: any;
|
||||||
|
setRowContent?: Dispatch<SetStateAction<any>>;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
updateRoute: string;
|
||||||
|
};
|
||||||
|
|
||||||
const Drawer: FunctionComponent<DrawerProps> = ({
|
const Drawer: FunctionComponent<DrawerProps> = ({
|
||||||
title,
|
titleKey,
|
||||||
children,
|
details,
|
||||||
onSave,
|
|
||||||
editableContent,
|
|
||||||
rowContent,
|
rowContent,
|
||||||
setData,
|
setRowContent,
|
||||||
}) => {
|
isAdmin,
|
||||||
|
updateRoute,
|
||||||
|
}: DrawerProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isFull, setIsFull] = useState(false);
|
const [isFull, setIsFull] = useState(false);
|
||||||
const [isFavorite, setIsFavorite] = useState(false);
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
const [tempRowContent, setTempRowContent] = useState(rowContent);
|
const [tempRowContent, setTempRowContent] = useState(rowContent);
|
||||||
|
|
||||||
const onRowUpdate = (updatedRow: any) => {
|
const handleTempRowContentChangeHTML = (
|
||||||
setData((prevData: any) =>
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
prevData.map((row: any) =>
|
) => {
|
||||||
row.id === updatedRow.id ? updatedRow : row
|
const { name, value } = e.target;
|
||||||
)
|
handleTempRowContentChange(name, value);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTempRowContentChange = (e) => {
|
const handleTempRowContentChange = (name: string, value: any) => {
|
||||||
const { name, value } = e.target;
|
setTempRowContent((prev: any) => ({
|
||||||
console.log(name);
|
...prev,
|
||||||
console.log(value);
|
|
||||||
setTempRowContent((prevContent) => ({
|
|
||||||
...prevContent,
|
|
||||||
[name]: value,
|
[name]: value,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnterPress = (e) => {
|
const handleEnterPress = (
|
||||||
|
e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
|
||||||
// Update the rowContent with the temporaryRowContent
|
|
||||||
if (onRowUpdate) {
|
|
||||||
onRowUpdate(tempRowContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async () => {
|
||||||
|
const response = await fetch(updateRoute, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(tempRowContent),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
const toggleDrawer = () => {
|
const toggleDrawer = () => {
|
||||||
|
if (setRowContent && isOpen) {
|
||||||
|
// Check if any values have changed
|
||||||
|
const hasChanges = Object.keys(tempRowContent).some(
|
||||||
|
(key) => tempRowContent[key] !== rowContent[key]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
handleUpdate().then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
setRowContent((prev: any) => {
|
||||||
|
return prev.map((row: any) => {
|
||||||
|
if (row.id === tempRowContent.id) {
|
||||||
|
return tempRowContent;
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
if (isFull) {
|
if (isFull) {
|
||||||
setIsFull(!isFull);
|
setIsFull(!isFull);
|
||||||
|
@ -99,34 +130,6 @@ const Drawer: FunctionComponent<DrawerProps> = ({
|
||||||
<OutlineStarIcon className="h-5 w-5" />
|
<OutlineStarIcon className="h-5 w-5" />
|
||||||
);
|
);
|
||||||
|
|
||||||
const [presetOptions, setPresetOptions] = useState([
|
|
||||||
"administrator",
|
|
||||||
"volunteer",
|
|
||||||
"employee",
|
|
||||||
]);
|
|
||||||
const [rolePresetOptions, setRolePresetOptions] = useState([
|
|
||||||
"domestic",
|
|
||||||
"community",
|
|
||||||
"economic",
|
|
||||||
]);
|
|
||||||
const [tagColors, setTagColors] = useState(new Map());
|
|
||||||
|
|
||||||
const getTagColor = (tag: string) => {
|
|
||||||
if (!tagColors.has(tag)) {
|
|
||||||
const colors = [
|
|
||||||
"bg-cyan-100",
|
|
||||||
"bg-blue-100",
|
|
||||||
"bg-green-100",
|
|
||||||
"bg-yellow-100",
|
|
||||||
"bg-purple-100",
|
|
||||||
];
|
|
||||||
const randomColor =
|
|
||||||
colors[Math.floor(Math.random() * colors.length)];
|
|
||||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
|
||||||
}
|
|
||||||
return tagColors.get(tag);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
@ -137,7 +140,6 @@ const Drawer: FunctionComponent<DrawerProps> = ({
|
||||||
>
|
>
|
||||||
Open
|
Open
|
||||||
</button>
|
</button>
|
||||||
<div className={drawerClassName}></div>
|
|
||||||
<div className={drawerClassName}>
|
<div className={drawerClassName}>
|
||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div className="flex flex-row items-center justify-between space-x-2">
|
<div className="flex flex-row items-center justify-between space-x-2">
|
||||||
|
@ -145,7 +147,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
</span>
|
</span>
|
||||||
<h2 className="text-lg text-gray-800 font-semibold">
|
<h2 className="text-lg text-gray-800 font-semibold">
|
||||||
{rowContent.username}
|
{rowContent[titleKey]}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -170,81 +172,202 @@ const Drawer: FunctionComponent<DrawerProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<table className="p-4">
|
<div className="flex flex-col space-y-3">
|
||||||
<tbody className="items-center">
|
{details.map((detail, index) => {
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
const value = tempRowContent[detail.key];
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
let valueToRender = <></>;
|
||||||
<td>
|
|
||||||
<UserIcon className="h-4 w-4" />
|
switch (detail.inputType) {
|
||||||
</td>
|
case "select-one":
|
||||||
<td className="w-32">Username</td>
|
valueToRender = (
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="rounded-md px-2 py-1">
|
||||||
|
{isAdmin ? (
|
||||||
|
<TagsInput
|
||||||
|
presetValue={
|
||||||
|
typeof value ===
|
||||||
|
"string"
|
||||||
|
? [value]
|
||||||
|
: value || []
|
||||||
|
}
|
||||||
|
presetOptions={
|
||||||
|
detail.presetOptionsValues ||
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
setPresetOptions={
|
||||||
|
detail.presetOptionsSetter ||
|
||||||
|
(() => {})
|
||||||
|
}
|
||||||
|
singleValue={true}
|
||||||
|
onTagsChange={(
|
||||||
|
tags: Set<string>
|
||||||
|
) => {
|
||||||
|
const tagsArray =
|
||||||
|
Array.from(
|
||||||
|
tags
|
||||||
|
);
|
||||||
|
handleTempRowContentChange(
|
||||||
|
detail.key,
|
||||||
|
tagsArray.length >
|
||||||
|
0
|
||||||
|
? tagsArray[0]
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex">
|
||||||
|
<Tag>
|
||||||
|
{value
|
||||||
|
? value
|
||||||
|
: "no value"}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "select-multiple":
|
||||||
|
valueToRender = (
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="rounded-md px-2 py-1">
|
||||||
|
{isAdmin ? (
|
||||||
|
<TagsInput
|
||||||
|
presetValue={
|
||||||
|
typeof value ===
|
||||||
|
"string"
|
||||||
|
? [value]
|
||||||
|
: value || []
|
||||||
|
}
|
||||||
|
presetOptions={
|
||||||
|
detail.presetOptionsValues ||
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
setPresetOptions={
|
||||||
|
detail.presetOptionsSetter ||
|
||||||
|
(() => {})
|
||||||
|
}
|
||||||
|
onTagsChange={(
|
||||||
|
tags: Set<string>
|
||||||
|
) => {
|
||||||
|
handleTempRowContentChange(
|
||||||
|
detail.key,
|
||||||
|
Array.from(tags)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{value &&
|
||||||
|
value.length > 0 ? (
|
||||||
|
value.map(
|
||||||
|
(
|
||||||
|
tag: string,
|
||||||
|
index: number
|
||||||
|
) => (
|
||||||
|
<Tag
|
||||||
|
key={
|
||||||
|
index
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Tag>
|
||||||
|
no requirements
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "textarea":
|
||||||
|
valueToRender = (
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="hover:bg-gray-50 rounded-md px-2 py-1">
|
||||||
|
<textarea
|
||||||
|
name={detail.key}
|
||||||
|
value={value}
|
||||||
|
onChange={
|
||||||
|
handleTempRowContentChangeHTML
|
||||||
|
}
|
||||||
|
onKeyDown={handleEnterPress}
|
||||||
|
rows={4}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
onInput={(e) => {
|
||||||
|
const target =
|
||||||
|
e.target as HTMLTextAreaElement;
|
||||||
|
target.style.height =
|
||||||
|
"auto";
|
||||||
|
target.style.height =
|
||||||
|
target.scrollHeight +
|
||||||
|
"px";
|
||||||
|
}}
|
||||||
|
className="w-full p-2 focus:outline-none border border-gray-200 rounded-md resize-none font-normal bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
valueToRender = (
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="hover:bg-gray-50 rounded-md px-2 py-1">
|
||||||
|
<input
|
||||||
|
type={detail.inputType}
|
||||||
|
name={detail.key}
|
||||||
|
value={value}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
onChange={
|
||||||
|
handleTempRowContentChangeHTML
|
||||||
|
}
|
||||||
|
onKeyDown={handleEnterPress}
|
||||||
|
className="w-full p-1 focus:outline-gray-200 bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "email":
|
||||||
|
valueToRender = (
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="hover:bg-gray-50 rounded-md px-2 py-1">
|
||||||
|
<input
|
||||||
|
type={detail.inputType}
|
||||||
|
name={detail.key}
|
||||||
|
value={value}
|
||||||
|
onChange={
|
||||||
|
handleTempRowContentChangeHTML
|
||||||
|
}
|
||||||
|
onKeyDown={handleEnterPress}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
className="w-full p-1 font-normal hover:text-gray-400 focus:outline-gray-200 underline text-gray-500 bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center text-xs gap-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center text-gray-500">
|
||||||
|
{detail.icon}
|
||||||
|
</div>
|
||||||
|
<div className="w-32">{detail.label}</div>
|
||||||
|
{valueToRender}
|
||||||
</div>
|
</div>
|
||||||
<td className="w-3/4 w-3/4 p-2 pl-0">
|
);
|
||||||
<input
|
})}
|
||||||
type="text"
|
</div>
|
||||||
name="username"
|
|
||||||
value={tempRowContent.username}
|
|
||||||
onChange={handleTempRowContentChange}
|
|
||||||
onKeyDown={handleEnterPress}
|
|
||||||
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
|
||||||
<td>
|
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
|
||||||
</td>
|
|
||||||
<td className="w-32">Role</td>
|
|
||||||
</div>
|
|
||||||
<td className="w-3/4 hover:bg-gray-50">
|
|
||||||
<TagsInput
|
|
||||||
presetValue={tempRowContent.role}
|
|
||||||
presetOptions={presetOptions}
|
|
||||||
setPresetOptions={setPresetOptions}
|
|
||||||
getTagColor={getTagColor}
|
|
||||||
setTagColors={setTagColors}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
|
||||||
<td>
|
|
||||||
<EnvelopeIcon className="h-4 w-4" />
|
|
||||||
</td>
|
|
||||||
<td className="w-32">Email</td>
|
|
||||||
</div>
|
|
||||||
<td className="w-3/4 p-2 pl-0">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="email"
|
|
||||||
value={tempRowContent.email}
|
|
||||||
onChange={handleTempRowContentChange}
|
|
||||||
onKeyDown={handleEnterPress}
|
|
||||||
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
|
||||||
<td>
|
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
|
||||||
</td>
|
|
||||||
<td className="w-32">Type of Program</td>
|
|
||||||
</div>
|
|
||||||
<td className="w-3/4 hover:bg-gray-50">
|
|
||||||
{/* {rowContent.program} */}
|
|
||||||
<TagsInput
|
|
||||||
presetValue={tempRowContent.program}
|
|
||||||
presetOptions={rolePresetOptions}
|
|
||||||
setPresetOptions={setRolePresetOptions}
|
|
||||||
getTagColor={getTagColor}
|
|
||||||
setTagColors={setTagColors}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const FilterBox = () => {
|
||||||
>
|
>
|
||||||
<span>{tag}</span>
|
<span>{tag}</span>
|
||||||
<span
|
<span
|
||||||
className="ml-2 cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => handleTagChange(tag)}
|
onClick={() => handleTagChange(tag)}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
|
67
compass/components/RootLayout.tsx
Normal file
67
compass/components/RootLayout.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Sidebar from "@/components/Sidebar/Sidebar";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import User, { Role } from "@/utils/models/User";
|
||||||
|
import Loading from "@/components/auth/Loading";
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||||
|
const [user, setUser] = useState<User | null>(null); // Initialize user as null
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getUser() {
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
const { data, error } = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
if (error || !data?.user) {
|
||||||
|
console.log("User not logged in or error fetching user");
|
||||||
|
router.push("/auth/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const user: User = await userData.json();
|
||||||
|
setUser(user); // Set user data after fetching
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Loading />; // Show loading screen while the user is being fetched
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
{/* Sidebar is shared across all pages */}
|
||||||
|
<Sidebar
|
||||||
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
|
isSidebarOpen={isSidebarOpen}
|
||||||
|
name={user.username}
|
||||||
|
email={user.email}
|
||||||
|
isAdmin={user.role === Role.ADMIN}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Page content */}
|
||||||
|
<div
|
||||||
|
className={`flex-1 transition duration-300 ease-in-out ${
|
||||||
|
isSidebarOpen ? "ml-64" : "ml-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children} {/* Render page-specific content here */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
ChevronDoubleLeftIcon,
|
ChevronDoubleLeftIcon,
|
||||||
|
@ -26,6 +26,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
email,
|
email,
|
||||||
isAdmin: admin,
|
isAdmin: admin,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Button to open the sidebar. */}
|
{/* Button to open the sidebar. */}
|
||||||
|
@ -62,11 +63,24 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-8">
|
{/* Loading indicator*/}
|
||||||
{/* user + logout button */}
|
{isLoading && (
|
||||||
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
|
<div className="fixed top-2 left-2">
|
||||||
<UserProfile name={name} email={email} />
|
<div className="flex justify-center items-center">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-purple-700" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-8">
|
||||||
|
<div className="flex items-center p-4 space-x-2 border rounded-md">
|
||||||
|
<UserProfile
|
||||||
|
name={name}
|
||||||
|
email={email}
|
||||||
|
setLoading={setIsLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* navigation menu */}
|
{/* navigation menu */}
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<h4 className="text-xs font-semibold text-gray-500">
|
<h4 className="text-xs font-semibold text-gray-500">
|
||||||
|
@ -79,6 +93,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
text="Admin"
|
text="Admin"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/admin"
|
redirect="/admin"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -87,24 +102,28 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
text="Home"
|
text="Home"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/home"
|
redirect="/home"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<BookmarkIcon />}
|
icon={<BookmarkIcon />}
|
||||||
text="Resources"
|
text="Resources"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/resource"
|
redirect="/resource"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<ClipboardIcon />}
|
icon={<ClipboardIcon />}
|
||||||
text="Services"
|
text="Services"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/service"
|
redirect="/service"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<BookOpenIcon />}
|
icon={<BookOpenIcon />}
|
||||||
text="Training Manuals"
|
text="Training Manuals"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/training-manuals"
|
redirect="/training-manuals"
|
||||||
|
onClick={setIsLoading}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
interface SidebarItemProps {
|
interface SidebarItemProps {
|
||||||
icon: React.ReactElement;
|
icon: React.ReactElement;
|
||||||
text: string;
|
text: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
redirect: string;
|
redirect: string;
|
||||||
|
onClick: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||||
|
@ -12,9 +14,15 @@ export const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||||
text,
|
text,
|
||||||
active,
|
active,
|
||||||
redirect,
|
redirect,
|
||||||
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
onClick={() =>
|
||||||
|
pathname.startsWith(redirect) ? onClick(false) : onClick(true)
|
||||||
|
}
|
||||||
href={redirect}
|
href={redirect}
|
||||||
className={
|
className={
|
||||||
active
|
active
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
import { Bars2Icon } from "@heroicons/react/24/solid";
|
import {
|
||||||
|
Bars2Icon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
LinkIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
||||||
import Table from "@/components/Table/Table";
|
import Table from "@/components/Table/Table";
|
||||||
import TagsInput from "@/components/TagsInput/Index";
|
|
||||||
import Resource from "@/utils/models/Resource";
|
import Resource from "@/utils/models/Resource";
|
||||||
|
import { Details } from "../Drawer/Drawer";
|
||||||
|
import { Tag } from "../TagsInput/Tag";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
type ResourceTableProps = {
|
type ResourceTableProps = {
|
||||||
data: Resource[];
|
data: Resource[];
|
||||||
setData: Dispatch<SetStateAction<Resource[]>>;
|
setData: Dispatch<SetStateAction<Resource[]>>;
|
||||||
|
user?: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,66 +24,126 @@ type ResourceTableProps = {
|
||||||
* @param props.data Stateful list of resources to be displayed by the table
|
* @param props.data Stateful list of resources to be displayed by the table
|
||||||
* @param props.setData State setter for the list of resources
|
* @param props.setData State setter for the list of resources
|
||||||
*/
|
*/
|
||||||
export default function ResourceTable({ data, setData }: ResourceTableProps) {
|
export default function ResourceTable({
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
user,
|
||||||
|
}: ResourceTableProps) {
|
||||||
const columnHelper = createColumnHelper<Resource>();
|
const columnHelper = createColumnHelper<Resource>();
|
||||||
|
|
||||||
// Set up tag handling
|
const [programPresets, setProgramPresets] = useState([
|
||||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
"DOMESTIC",
|
||||||
|
"COMMUNITY",
|
||||||
|
"ECONOMIC",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const resourceDetails: Details[] = [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "name",
|
||||||
|
inputType: "text",
|
||||||
|
icon: <UserIcon className="h-4 w-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "link",
|
||||||
|
label: "link",
|
||||||
|
inputType: "email",
|
||||||
|
icon: <LinkIcon className="h-4 w-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "program",
|
||||||
|
label: "program",
|
||||||
|
inputType: "select-one",
|
||||||
|
icon: <ListBulletIcon className="h-4 w-4" />,
|
||||||
|
presetOptionsValues: programPresets,
|
||||||
|
presetOptionsSetter: setProgramPresets,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "summary",
|
||||||
|
label: "summary",
|
||||||
|
inputType: "textarea",
|
||||||
|
icon: <DocumentTextIcon className="h-4 w-4" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
// Define Tanstack columns
|
// Define Tanstack columns
|
||||||
const columns: ColumnDef<Resource, any>[] = [
|
const columns: ColumnDef<Resource, any>[] = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Name
|
<UserIcon className="inline align-top h-4" /> Name
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<RowOpenAction
|
<RowOpenAction
|
||||||
title={info.getValue()}
|
title={info.getValue()}
|
||||||
|
titleKey="name"
|
||||||
rowData={info.row.original}
|
rowData={info.row.original}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
|
details={resourceDetails}
|
||||||
|
isAdmin={user?.role === "ADMIN"}
|
||||||
|
updateRoute={`/api/resource/update?uuid=${user?.uuid}`}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("link", {
|
columnHelper.accessor("link", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Link
|
<LinkIcon className="inline align-top h-4" /> Link
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<a
|
<div className="flex items-start gap-2 px-2">
|
||||||
href={info.getValue()}
|
<a
|
||||||
target={"_blank"}
|
href={info.getValue()}
|
||||||
className="ml-2 text-gray-500 underline hover:text-gray-400"
|
target="_blank"
|
||||||
>
|
className="text-gray-500 underline hover:text-gray-400 break-all"
|
||||||
{info.getValue()}
|
>
|
||||||
</a>
|
{info.getValue()}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("program", {
|
columnHelper.accessor("program", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Program
|
<ListBulletIcon className="inline align-top h-4" /> Program
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
<div className="flex flex-wrap gap-2 items-center px-2">
|
||||||
|
<Tag>
|
||||||
|
{info.getValue().length != 0
|
||||||
|
? info.getValue()
|
||||||
|
: "no program"}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor("summary", {
|
columnHelper.accessor("summary", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
<DocumentTextIcon className="inline align-top h-4" />{" "}
|
||||||
|
Summary
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
<div className="flex items-start gap-2 px-2 py-1">
|
||||||
|
<span className="text-gray-500 max-h-8 overflow-y-auto">
|
||||||
|
{info.getValue()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Table data={data} setData={setData} columns={columns} />;
|
return (
|
||||||
|
<Table
|
||||||
|
data={data}
|
||||||
|
setData={setData}
|
||||||
|
columns={columns}
|
||||||
|
details={resourceDetails}
|
||||||
|
createEndpoint={`/api/resource/create?uuid=${user?.uuid}`}
|
||||||
|
isAdmin={user?.role === "ADMIN"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,43 @@
|
||||||
import Drawer from "@/components/Drawer/Drawer";
|
import Drawer, { Details } from "@/components/Drawer/Drawer";
|
||||||
import DataPoint from "@/utils/models/DataPoint";
|
import DataPoint from "@/utils/models/DataPoint";
|
||||||
|
import {
|
||||||
|
EnvelopeIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
|
|
||||||
type RowOpenActionProps<T extends DataPoint> = {
|
type RowOpenActionProps<T extends DataPoint> = {
|
||||||
title: string;
|
title: string;
|
||||||
|
titleKey: string;
|
||||||
rowData: T;
|
rowData: T;
|
||||||
setData: Dispatch<SetStateAction<T[]>>;
|
setData: Dispatch<SetStateAction<T[]>>;
|
||||||
|
details: Details[];
|
||||||
|
isAdmin?: boolean;
|
||||||
|
updateRoute: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RowOpenAction<T extends DataPoint>({
|
export function RowOpenAction<T extends DataPoint>({
|
||||||
title,
|
title,
|
||||||
|
titleKey,
|
||||||
rowData,
|
rowData,
|
||||||
setData,
|
setData,
|
||||||
|
details,
|
||||||
|
isAdmin,
|
||||||
|
updateRoute,
|
||||||
}: RowOpenActionProps<T>) {
|
}: RowOpenActionProps<T>) {
|
||||||
const [pageContent, setPageContent] = useState("");
|
|
||||||
|
|
||||||
const handleDrawerContentChange = (newContent: string) => {
|
|
||||||
setPageContent(newContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
|
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
|
||||||
{title}
|
{title}
|
||||||
<span>
|
<span>
|
||||||
<Drawer
|
<Drawer
|
||||||
title="My Drawer Title"
|
titleKey={titleKey}
|
||||||
editableContent={pageContent}
|
|
||||||
rowContent={rowData}
|
rowContent={rowData}
|
||||||
onSave={handleDrawerContentChange}
|
details={details}
|
||||||
setData={setData}
|
setRowContent={setData}
|
||||||
>
|
isAdmin={isAdmin}
|
||||||
{pageContent}
|
updateRoute={updateRoute}
|
||||||
</Drawer>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
import { Bars2Icon } from "@heroicons/react/24/solid";
|
import {
|
||||||
import { Dispatch, SetStateAction } from "react";
|
Bars2Icon,
|
||||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
CheckCircleIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||||
import Table from "@/components/Table/Table";
|
import Table from "@/components/Table/Table";
|
||||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
||||||
import TagsInput from "@/components/TagsInput/Index";
|
|
||||||
import Service from "@/utils/models/Service";
|
import Service from "@/utils/models/Service";
|
||||||
|
import { Details } from "../Drawer/Drawer";
|
||||||
|
import { Tag } from "../TagsInput/Tag";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
|
|
||||||
type ServiceTableProps = {
|
type ServiceTableProps = {
|
||||||
data: Service[];
|
data: Service[];
|
||||||
setData: Dispatch<SetStateAction<Service[]>>;
|
setData: Dispatch<SetStateAction<Service[]>>;
|
||||||
|
user?: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,87 +25,177 @@ type ServiceTableProps = {
|
||||||
* @param props.data Stateful list of services to be displayed by the table
|
* @param props.data Stateful list of services to be displayed by the table
|
||||||
* @param props.setData State setter for the list of services
|
* @param props.setData State setter for the list of services
|
||||||
*/
|
*/
|
||||||
export default function ServiceTable({ data, setData }: ServiceTableProps) {
|
export default function ServiceTable({
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
user,
|
||||||
|
}: ServiceTableProps) {
|
||||||
const columnHelper = createColumnHelper<Service>();
|
const columnHelper = createColumnHelper<Service>();
|
||||||
|
|
||||||
// Set up tag handling
|
const [programPresets, setProgramPresets] = useState([
|
||||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
"DOMESTIC",
|
||||||
|
"COMMUNITY",
|
||||||
// TODO: Dynamically or statically get full list of preset requirement tag options
|
"ECONOMIC",
|
||||||
const requirementProps = useTagsHandler([
|
|
||||||
"anonymous",
|
|
||||||
"confidential",
|
|
||||||
"referral required",
|
|
||||||
"safety assessment",
|
|
||||||
"intake required",
|
|
||||||
"income eligibility",
|
|
||||||
"initial assessment",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const [requirementPresets, setRequirementPresets] = useState([
|
||||||
|
"Anonymous",
|
||||||
|
"Confidential",
|
||||||
|
"Referral required",
|
||||||
|
"Safety assessment",
|
||||||
|
"Intake required",
|
||||||
|
"Income eligibility",
|
||||||
|
"Initial assessment",
|
||||||
|
"Insurance accepted",
|
||||||
|
"Open to parents",
|
||||||
|
"18+",
|
||||||
|
"Application required",
|
||||||
|
"Proof of income",
|
||||||
|
"Background check",
|
||||||
|
"Enrollment required",
|
||||||
|
"Registration required",
|
||||||
|
"Parental consent",
|
||||||
|
"Age-appropriate",
|
||||||
|
"Collaborative",
|
||||||
|
"Open to the public",
|
||||||
|
"Registration preferred",
|
||||||
|
"Legal case",
|
||||||
|
"Scheduling required",
|
||||||
|
"Limited availability",
|
||||||
|
"Eligibility assessment",
|
||||||
|
"Pre-registration required",
|
||||||
|
"Commitment to attend",
|
||||||
|
"Training required",
|
||||||
|
"Based on individual needs",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const serviceDetails: Details[] = [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "name",
|
||||||
|
inputType: "text",
|
||||||
|
icon: <UserIcon className="inline align-top h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "status",
|
||||||
|
inputType: "text",
|
||||||
|
icon: <CheckCircleIcon className="inline align-top h-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "program",
|
||||||
|
label: "program",
|
||||||
|
inputType: "select-one",
|
||||||
|
icon: <ListBulletIcon className="inline align-top h-4" />,
|
||||||
|
presetOptionsValues: programPresets,
|
||||||
|
presetOptionsSetter: setProgramPresets,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "requirements",
|
||||||
|
label: "requirements",
|
||||||
|
inputType: "select-multiple",
|
||||||
|
icon: <ListBulletIcon className="inline align-top h-4" />,
|
||||||
|
presetOptionsValues: requirementPresets,
|
||||||
|
presetOptionsSetter: setRequirementPresets,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "summary",
|
||||||
|
label: "summary",
|
||||||
|
inputType: "textarea",
|
||||||
|
icon: <DocumentTextIcon className="inline align-top h-4" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Define Tanstack columns
|
// Define Tanstack columns
|
||||||
const columns: ColumnDef<Service, any>[] = [
|
const columns: ColumnDef<Service, any>[] = [
|
||||||
columnHelper.accessor("name", {
|
columnHelper.accessor("name", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Name
|
<UserIcon className="inline align-top h-4" /> Name
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<RowOpenAction
|
<RowOpenAction
|
||||||
title={info.getValue()}
|
title={info.getValue()}
|
||||||
|
titleKey="name"
|
||||||
rowData={info.row.original}
|
rowData={info.row.original}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
|
details={serviceDetails}
|
||||||
|
updateRoute={`/api/service/update?uuid=${user?.uuid}`}
|
||||||
|
isAdmin={user?.role === "ADMIN"}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("status", {
|
columnHelper.accessor("status", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Status
|
<CheckCircleIcon className="inline align-top h-4" /> Status
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
<span className="text-gray-500 px-2">{info.getValue()}</span>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("program", {
|
columnHelper.accessor("program", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Program
|
<ListBulletIcon className="inline align-top h-4" /> Program
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
<div className="flex flex-wrap gap-2 items-center px-2">
|
||||||
|
<Tag>
|
||||||
|
{info.getValue().length != 0
|
||||||
|
? info.getValue()
|
||||||
|
: "no program"}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("requirements", {
|
columnHelper.accessor("requirements", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Requirements
|
<ListBulletIcon className="inline align-top h-4" />{" "}
|
||||||
|
Requirements
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
// TODO: Setup different tag handler for requirements
|
<div className="flex flex-wrap gap-2 items-center p-2">
|
||||||
<TagsInput
|
{info.getValue().length > 0 ? (
|
||||||
presetValue={
|
info.getValue().map((tag: string, index: number) => {
|
||||||
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
|
return <Tag key={index}>{tag}</Tag>;
|
||||||
}
|
})
|
||||||
{...requirementProps}
|
) : (
|
||||||
/>
|
<Tag>no requirements</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
columnHelper.accessor("summary", {
|
columnHelper.accessor("summary", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
<DocumentTextIcon className="inline align-top h-4" />{" "}
|
||||||
|
Summary
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
<div className="flex items-start gap-2 px-2 py-1">
|
||||||
|
<span className="text-gray-500 max-h-8 overflow-y-auto">
|
||||||
|
{info.getValue()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Table data={data} setData={setData} columns={columns} />;
|
return (
|
||||||
|
<Table
|
||||||
|
data={data}
|
||||||
|
setData={setData}
|
||||||
|
columns={columns}
|
||||||
|
details={serviceDetails}
|
||||||
|
createEndpoint={`/api/service/create?uuid=${user?.uuid}`}
|
||||||
|
isAdmin={user?.role === "ADMIN"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,35 @@ import { PlusIcon } from "@heroicons/react/24/solid";
|
||||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||||
import { RowOptionMenu } from "./RowOptionMenu";
|
import { RowOptionMenu } from "./RowOptionMenu";
|
||||||
import DataPoint from "@/utils/models/DataPoint";
|
import DataPoint from "@/utils/models/DataPoint";
|
||||||
|
import CreateDrawer from "../Drawer/CreateDrawer";
|
||||||
|
import { Details } from "../Drawer/Drawer";
|
||||||
|
|
||||||
type TableProps<T extends DataPoint> = {
|
type TableProps<T extends DataPoint> = {
|
||||||
data: T[];
|
data: T[];
|
||||||
setData: Dispatch<SetStateAction<T[]>>;
|
setData: Dispatch<SetStateAction<T[]>>;
|
||||||
columns: ColumnDef<T, any>[];
|
columns: ColumnDef<T, any>[];
|
||||||
|
details: Details[];
|
||||||
|
createEndpoint: string;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Validates that all required fields in a new item have values */
|
||||||
|
const validateNewItem = (newItem: any, details: Details[]): boolean => {
|
||||||
|
const hasEmptyFields = details.some((detail) => {
|
||||||
|
const value = newItem[detail.key];
|
||||||
|
return (
|
||||||
|
value === undefined ||
|
||||||
|
value === null ||
|
||||||
|
value === "" ||
|
||||||
|
(Array.isArray(value) && value.length === 0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasEmptyFields) {
|
||||||
|
alert("Please fill in all fields before creating a new item");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Fuzzy search function */
|
/** Fuzzy search function */
|
||||||
|
@ -53,46 +77,59 @@ export default function Table<T extends DataPoint>({
|
||||||
data,
|
data,
|
||||||
setData,
|
setData,
|
||||||
columns,
|
columns,
|
||||||
|
details,
|
||||||
|
createEndpoint,
|
||||||
|
isAdmin = false,
|
||||||
}: TableProps<T>) {
|
}: TableProps<T>) {
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<T>();
|
const columnHelper = createColumnHelper<T>();
|
||||||
|
|
||||||
/** Sorting function based on visibility */
|
const createRow = async (newItem: any) => {
|
||||||
const visibilitySort = (a: T, b: T) =>
|
const response = await fetch(createEndpoint, {
|
||||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1;
|
method: "POST",
|
||||||
|
headers: {
|
||||||
// Sort data on load
|
"Content-Type": "application/json",
|
||||||
useEffect(() => {
|
},
|
||||||
setData((prevData) => prevData.sort(visibilitySort));
|
body: JSON.stringify(newItem),
|
||||||
}, [setData]);
|
|
||||||
|
|
||||||
// Data manipulation methods
|
|
||||||
// TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
|
|
||||||
const deleteData = (dataId: number) => {
|
|
||||||
console.log(data);
|
|
||||||
setData((currentData) =>
|
|
||||||
currentData.filter((data) => data.id !== dataId)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideData = (dataId: number) => {
|
|
||||||
console.log(`Toggling visibility for data with ID: ${dataId}`);
|
|
||||||
setData((currentData) => {
|
|
||||||
const newData = currentData
|
|
||||||
.map((data) =>
|
|
||||||
data.id === dataId
|
|
||||||
? { ...data, visible: !data.visible }
|
|
||||||
: data
|
|
||||||
)
|
|
||||||
.sort(visibilitySort);
|
|
||||||
|
|
||||||
console.log(newData);
|
|
||||||
return newData;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addData = () => {
|
// /** Sorting function based on visibility */
|
||||||
setData([...data]);
|
// const visibilitySort = (a: T, b: T) =>
|
||||||
};
|
// a.visible === b.visible ? 0 : a.visible ? -1 : 1;
|
||||||
|
|
||||||
|
// // Sort data on load
|
||||||
|
// useEffect(() => {
|
||||||
|
// setData((prevData) => prevData.sort(visibilitySort));
|
||||||
|
// }, [setData]);
|
||||||
|
|
||||||
|
// // Data manipulation methods
|
||||||
|
// // TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
|
||||||
|
// const deleteData = (dataId: number) => {
|
||||||
|
// console.log(data);
|
||||||
|
// setData((currentData) =>
|
||||||
|
// currentData.filter((data) => data.id !== dataId)
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const hideData = (dataId: number) => {
|
||||||
|
// console.log(`Toggling visibility for data with ID: ${dataId}`);
|
||||||
|
// setData((currentData) => {
|
||||||
|
// const newData = currentData
|
||||||
|
// .map((data) =>
|
||||||
|
// data.id === dataId
|
||||||
|
// ? { ...data, visible: !data.visible }
|
||||||
|
// : data
|
||||||
|
// )
|
||||||
|
// .sort(visibilitySort);
|
||||||
|
|
||||||
|
// console.log(newData);
|
||||||
|
// return newData;
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
// Add data manipulation options to the first column
|
// Add data manipulation options to the first column
|
||||||
columns.unshift(
|
columns.unshift(
|
||||||
|
@ -100,8 +137,10 @@ export default function Table<T extends DataPoint>({
|
||||||
id: "options",
|
id: "options",
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RowOptionMenu
|
<RowOptionMenu
|
||||||
onDelete={() => deleteData(props.row.original.id)}
|
onDelete={() => {}}
|
||||||
onHide={() => hideData(props.row.original.id)}
|
onHide={() => {}}
|
||||||
|
// onDelete={() => deleteData(props.row.original.id)}
|
||||||
|
// onHide={() => hideData(props.row.original.id)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
@ -114,11 +153,6 @@ export default function Table<T extends DataPoint>({
|
||||||
setQuery(String(target.value));
|
setQuery(String(target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
console.log(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Filtering
|
// TODO: Filtering
|
||||||
|
|
||||||
// TODO: Sorting
|
// TODO: Sorting
|
||||||
|
@ -138,16 +172,6 @@ export default function Table<T extends DataPoint>({
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRowData = (row: any) => {
|
|
||||||
const rowData: any = {};
|
|
||||||
row.cells.forEach((cell: any) => {
|
|
||||||
rowData[cell.column.id] = cell.value;
|
|
||||||
});
|
|
||||||
// Use rowData object containing data from all columns for the current row
|
|
||||||
console.log(rowData);
|
|
||||||
return rowData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
|
@ -206,18 +230,37 @@ export default function Table<T extends DataPoint>({
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
{isAdmin && ( // Only show create drawer for admins
|
||||||
<td
|
<tr>
|
||||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
<td
|
||||||
colSpan={100}
|
className="p-3 border-y border-gray-200"
|
||||||
onClick={addData}
|
colSpan={100}
|
||||||
>
|
>
|
||||||
<span className="flex ml-1 text-gray-500">
|
<CreateDrawer
|
||||||
<PlusIcon className="inline h-4 mr-1" />
|
details={details}
|
||||||
New
|
onCreate={(newItem) => {
|
||||||
</span>
|
if (
|
||||||
</td>
|
!validateNewItem(newItem, details)
|
||||||
</tr>
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRow(newItem).then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
newItem.visible = true;
|
||||||
|
setData((prev) => [
|
||||||
|
...prev,
|
||||||
|
newItem,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import {
|
import {
|
||||||
ArrowDownCircleIcon,
|
EnvelopeIcon,
|
||||||
AtSymbolIcon,
|
ListBulletIcon,
|
||||||
Bars2Icon,
|
UserIcon,
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
||||||
import Table from "@/components/Table/Table";
|
import Table from "@/components/Table/Table";
|
||||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
||||||
import TagsInput from "@/components/TagsInput/Index";
|
|
||||||
import User from "@/utils/models/User";
|
import User from "@/utils/models/User";
|
||||||
|
import { Details } from "../Drawer/Drawer";
|
||||||
|
import { Tag } from "../TagsInput/Tag";
|
||||||
|
|
||||||
type UserTableProps = {
|
type UserTableProps = {
|
||||||
data: User[];
|
data: User[];
|
||||||
setData: Dispatch<SetStateAction<User[]>>;
|
setData: Dispatch<SetStateAction<User[]>>;
|
||||||
|
user?: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,49 +22,92 @@ type UserTableProps = {
|
||||||
* @param props.data Stateful list of users to be displayed by the table
|
* @param props.data Stateful list of users to be displayed by the table
|
||||||
* @param props.setData State setter for the list of users
|
* @param props.setData State setter for the list of users
|
||||||
*/
|
*/
|
||||||
export default function UserTable({ data, setData }: UserTableProps) {
|
export default function UserTable({ data, setData, user }: UserTableProps) {
|
||||||
const columnHelper = createColumnHelper<User>();
|
const columnHelper = createColumnHelper<User>();
|
||||||
|
|
||||||
// Set up tag handling
|
const [rolePresets, setRolePresets] = useState([
|
||||||
const roleProps = useTagsHandler([
|
"ADMIN",
|
||||||
"administrator",
|
"VOLUNTEER",
|
||||||
"volunteer",
|
"EMPLOYEE",
|
||||||
"employee",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
const [programPresets, setProgramPresets] = useState([
|
||||||
|
"DOMESTIC",
|
||||||
|
"COMMUNITY",
|
||||||
|
"ECONOMIC",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userDetails: Details[] = [
|
||||||
|
{
|
||||||
|
key: "username",
|
||||||
|
label: "username",
|
||||||
|
inputType: "text",
|
||||||
|
icon: <UserIcon className="h-4 w-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "role",
|
||||||
|
label: "role",
|
||||||
|
inputType: "select-one",
|
||||||
|
icon: <ListBulletIcon className="h-4 w-4" />,
|
||||||
|
presetOptionsValues: rolePresets,
|
||||||
|
presetOptionsSetter: setRolePresets,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: "email",
|
||||||
|
inputType: "email",
|
||||||
|
icon: <EnvelopeIcon className="h-4 w-4" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "program",
|
||||||
|
label: "program",
|
||||||
|
inputType: "select-multiple",
|
||||||
|
icon: <ListBulletIcon className="h-4 w-4" />,
|
||||||
|
presetOptionsValues: programPresets,
|
||||||
|
presetOptionsSetter: setProgramPresets,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Define Tanstack columns
|
// Define Tanstack columns
|
||||||
const columns: ColumnDef<User, any>[] = [
|
const columns: ColumnDef<User, any>[] = [
|
||||||
columnHelper.accessor("username", {
|
columnHelper.accessor("username", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<Bars2Icon className="inline align-top h-4" /> Username
|
<UserIcon className="inline align-top h-4" /> Username
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<RowOpenAction
|
<RowOpenAction
|
||||||
title={info.getValue()}
|
title={info.getValue()}
|
||||||
|
titleKey="username"
|
||||||
rowData={info.row.original}
|
rowData={info.row.original}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
|
details={userDetails}
|
||||||
|
isAdmin={user?.role === "ADMIN"}
|
||||||
|
updateRoute={`/api/user/update?uuid=${user?.uuid}`}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("role", {
|
columnHelper.accessor("role", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
<ListBulletIcon className="inline align-top h-4" /> Role
|
||||||
Role
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<TagsInput presetValue={info.getValue()} {...roleProps} />
|
<div className="flex flex-wrap gap-2 items-center px-2">
|
||||||
|
<Tag>
|
||||||
|
{info.getValue() && info.getValue().length != 0
|
||||||
|
? info.getValue()
|
||||||
|
: "no role"}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor("email", {
|
columnHelper.accessor("email", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<AtSymbolIcon className="inline align-top h-4" /> Email
|
<EnvelopeIcon className="inline align-top h-4" /> Email
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
|
@ -75,15 +119,31 @@ export default function UserTable({ data, setData }: UserTableProps) {
|
||||||
columnHelper.accessor("program", {
|
columnHelper.accessor("program", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<>
|
<>
|
||||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
<ListBulletIcon className="inline align-top h-4" /> Program
|
||||||
Program
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
<div className="flex p-2 flex-wrap gap-2 items-center">
|
||||||
|
{info.getValue().length > 0 ? (
|
||||||
|
info.getValue().map((tag: string, index: number) => {
|
||||||
|
return <Tag key={index}>{tag}</Tag>;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Tag>no programs</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Table<User> data={data} setData={setData} columns={columns} />;
|
return (
|
||||||
|
<Table<User>
|
||||||
|
data={data}
|
||||||
|
setData={setData}
|
||||||
|
columns={columns}
|
||||||
|
details={userDetails}
|
||||||
|
createEndpoint={`/api/user/create?uuid=${user?.uuid}`}
|
||||||
|
isAdmin={user?.role === "ADMIN"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import { Tag } from "./Tag";
|
import { Tag } from "./Tag";
|
||||||
|
|
||||||
export const CreateNewTagAction = ({ input }) => {
|
interface NewTagProps {
|
||||||
|
input: string;
|
||||||
|
addTag: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateNewTagAction = ({ input, addTag }: NewTagProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row space-x-2 hover:bg-gray-100 rounded-md py-2 p-2 items-center">
|
<button
|
||||||
|
className="flex flex-row space-x-2 hover:bg-gray-100 rounded-md py-2 p-2 items-center"
|
||||||
|
onClick={addTag}
|
||||||
|
>
|
||||||
<p className="capitalize">Create</p>
|
<p className="capitalize">Create</p>
|
||||||
<Tag active={false} onDelete={null}>
|
<Tag active={false}>{input}</Tag>
|
||||||
{input}
|
</button>
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
|
import { EllipsisHorizontalIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const DropdownAction = ({ tag, handleDeleteTag, handleEditTag }) => {
|
interface DropdownActionProps {
|
||||||
|
tag: string;
|
||||||
|
handleDeleteTag: (tag: string) => void;
|
||||||
|
handleEditTag: (oldTag: string, newTag: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropdownAction = ({
|
||||||
|
tag,
|
||||||
|
handleDeleteTag,
|
||||||
|
handleEditTag,
|
||||||
|
}: DropdownActionProps) => {
|
||||||
const [isVisible, setVisible] = useState(false);
|
const [isVisible, setVisible] = useState(false);
|
||||||
const [inputValue, setInputValue] = useState(tag);
|
const [inputValue, setInputValue] = useState(tag);
|
||||||
|
|
||||||
const editTagOption = (e) => {
|
const editTagOption = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
handleEditTag(tag, inputValue);
|
handleEditTag(tag, inputValue);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
|
|
|
@ -6,51 +6,52 @@ import { CreateNewTagAction } from "./CreateNewTagAction";
|
||||||
|
|
||||||
interface TagsInputProps {
|
interface TagsInputProps {
|
||||||
presetOptions: string[];
|
presetOptions: string[];
|
||||||
presetValue: string | string[];
|
presetValue: string[];
|
||||||
setPresetOptions: Dispatch<SetStateAction<string[]>>;
|
setPresetOptions: Dispatch<SetStateAction<string[]>>;
|
||||||
getTagColor(tag: string): string;
|
onTagsChange?: (tags: Set<string>) => void;
|
||||||
|
singleValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagsInput: React.FC<TagsInputProps> = ({
|
const TagsInput: React.FC<TagsInputProps> = ({
|
||||||
presetValue,
|
presetValue,
|
||||||
presetOptions,
|
presetOptions,
|
||||||
setPresetOptions,
|
setPresetOptions,
|
||||||
getTagColor,
|
onTagsChange,
|
||||||
|
singleValue = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [cellSelected, setCellSelected] = useState(false);
|
const [cellSelected, setCellSelected] = useState(false);
|
||||||
const [tags, setTags] = useState<Set<string>>(
|
|
||||||
typeof presetValue === "string"
|
// TODO: Add tags to the database and remove the presetValue and lowercasing
|
||||||
? new Set([presetValue])
|
const [tags, setTags] = useState<Set<string>>(new Set(presetValue));
|
||||||
: new Set(presetValue)
|
|
||||||
);
|
|
||||||
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
|
const [options, setOptions] = useState<Set<string>>(new Set(presetOptions));
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState<Set<string>>(
|
||||||
|
new Set(presetOptions)
|
||||||
|
);
|
||||||
const dropdown = useRef<HTMLDivElement>(null);
|
const dropdown = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!cellSelected) {
|
if (!cellSelected) {
|
||||||
setCellSelected(true);
|
setCellSelected(true);
|
||||||
// Add event listener only after setting cellSelected to true
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.addEventListener("click", handleOutsideClick);
|
window.addEventListener("click", handleOutsideClick);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Fix MouseEvent type and remove the as Node as that is completely wrong
|
|
||||||
const handleOutsideClick = (event: MouseEvent) => {
|
const handleOutsideClick = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
dropdown.current &&
|
dropdown.current &&
|
||||||
!dropdown.current.contains(event.target as Node)
|
!dropdown.current.contains(event.target as Node)
|
||||||
) {
|
) {
|
||||||
|
console.log("outside");
|
||||||
setCellSelected(false);
|
setCellSelected(false);
|
||||||
// Remove event listener after handling outside click
|
|
||||||
window.removeEventListener("click", handleOutsideClick);
|
window.removeEventListener("click", handleOutsideClick);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setOptions(() => {
|
setFilteredOptions(() => {
|
||||||
const newOptions = presetOptions.filter((item) =>
|
const newOptions = presetOptions.filter((item) =>
|
||||||
item.includes(e.target.value.toLowerCase())
|
item.includes(e.target.value.toLowerCase())
|
||||||
);
|
);
|
||||||
|
@ -61,39 +62,58 @@ const TagsInput: React.FC<TagsInputProps> = ({
|
||||||
|
|
||||||
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === "Enter" && inputValue.trim()) {
|
if (e.key === "Enter" && inputValue.trim()) {
|
||||||
// setPresetOptions((prevPreset) => {
|
if (singleValue && tags.size >= 1) {
|
||||||
// const uniqueSet = new Set(presetOptions);
|
// Don't add new tag if we're in single value mode and already have a tag
|
||||||
// uniqueSet.add(inputValue);
|
return;
|
||||||
// return Array.from(uniqueSet);
|
}
|
||||||
// });
|
addTag(e);
|
||||||
setTags((prevTags) => new Set(prevTags).add(inputValue));
|
|
||||||
setOptions((prevOptions) => new Set(prevOptions).add(inputValue));
|
|
||||||
setInputValue("");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addTag = (e?: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
|
||||||
|
const newTags = new Set(Array.from(tags).concat(inputValue));
|
||||||
|
setOptions(new Set(Array.from(options).concat(inputValue)));
|
||||||
|
setTags(newTags);
|
||||||
|
setFilteredOptions(new Set(Array.from(options).concat(inputValue)));
|
||||||
|
setInputValue("");
|
||||||
|
onTagsChange?.(newTags);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectTag = (tagToAdd: string) => {
|
const handleSelectTag = (tagToAdd: string) => {
|
||||||
if (!tags.has(tagToAdd)) {
|
if (singleValue) {
|
||||||
// Corrected syntax for checking if a Set contains an item
|
const newTags = new Set([tagToAdd]);
|
||||||
setTags((prevTags) => new Set(prevTags).add(tagToAdd));
|
setTags(newTags);
|
||||||
|
onTagsChange?.(newTags);
|
||||||
|
} else if (!tags.has(tagToAdd)) {
|
||||||
|
const newTags = new Set(Array.from(tags).concat(tagToAdd));
|
||||||
|
setTags(newTags);
|
||||||
|
onTagsChange?.(newTags);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteTag = (tagToDelete: string) => {
|
const handleDeleteTag = (tagToDelete: string) => {
|
||||||
setTags((prevTags) => {
|
const newTags = new Set(
|
||||||
const updatedTags = new Set(prevTags);
|
Array.from(tags).filter((tag) => tag !== tagToDelete)
|
||||||
updatedTags.delete(tagToDelete);
|
);
|
||||||
return updatedTags;
|
setTags(newTags);
|
||||||
});
|
onTagsChange?.(newTags);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteTagOption = (tagToDelete: string) => {
|
const handleDeleteTagOption = (tagToDelete: string) => {
|
||||||
// setPresetOptions(presetOptions.filter(tag => tag !== tagToDelete));
|
setOptions(
|
||||||
setOptions((prevOptions) => {
|
new Set(
|
||||||
const updatedOptions = new Set(prevOptions);
|
Array.from(options).filter((option) => option !== tagToDelete)
|
||||||
updatedOptions.delete(tagToDelete);
|
)
|
||||||
return updatedOptions;
|
);
|
||||||
});
|
|
||||||
|
setFilteredOptions(
|
||||||
|
new Set(
|
||||||
|
Array.from(options).filter((option) => option !== tagToDelete)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (tags.has(tagToDelete)) {
|
if (tags.has(tagToDelete)) {
|
||||||
handleDeleteTag(tagToDelete);
|
handleDeleteTag(tagToDelete);
|
||||||
}
|
}
|
||||||
|
@ -101,23 +121,20 @@ const TagsInput: React.FC<TagsInputProps> = ({
|
||||||
|
|
||||||
const handleEditTag = (oldTag: string, newTag: string) => {
|
const handleEditTag = (oldTag: string, newTag: string) => {
|
||||||
if (oldTag !== newTag) {
|
if (oldTag !== newTag) {
|
||||||
setTags((prevTags) => {
|
setTags(
|
||||||
const tagsArray = Array.from(prevTags);
|
new Set(
|
||||||
const oldTagIndex = tagsArray.indexOf(oldTag);
|
Array.from(tags)
|
||||||
if (oldTagIndex !== -1) {
|
.filter((tag) => tag !== oldTag)
|
||||||
tagsArray.splice(oldTagIndex, 1, newTag);
|
.concat(newTag)
|
||||||
}
|
)
|
||||||
return new Set(tagsArray);
|
);
|
||||||
});
|
setOptions(
|
||||||
|
new Set(
|
||||||
setOptions((prevOptions) => {
|
Array.from(options)
|
||||||
const optionsArray = Array.from(prevOptions);
|
.filter((option) => option !== oldTag)
|
||||||
const oldTagIndex = optionsArray.indexOf(oldTag);
|
.concat(newTag)
|
||||||
if (oldTagIndex !== -1) {
|
)
|
||||||
optionsArray.splice(oldTagIndex, 1, newTag);
|
);
|
||||||
}
|
|
||||||
return new Set(optionsArray);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -139,28 +156,45 @@ const TagsInput: React.FC<TagsInputProps> = ({
|
||||||
active
|
active
|
||||||
tags={tags}
|
tags={tags}
|
||||||
/>
|
/>
|
||||||
<input
|
{(!singleValue || tags.size === 0) && (
|
||||||
type="text"
|
<input
|
||||||
value={inputValue}
|
type="text"
|
||||||
placeholder="Search for an option..."
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
placeholder={
|
||||||
onKeyDown={handleAddTag}
|
singleValue && tags.size > 0
|
||||||
className="focus:outline-none bg-transparent"
|
? ""
|
||||||
autoFocus
|
: "Search for an option..."
|
||||||
/>
|
}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleAddTag}
|
||||||
|
className="focus:outline-none bg-transparent"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex rounded-b-md bg-white flex-col border-t border-gray-100 text-2xs font-medium text-gray-500 p-2">
|
<div className="flex rounded-b-md bg-white flex-col border-t border-gray-100 text-2xs font-medium text-gray-500 p-2">
|
||||||
<p className="capitalize">
|
<p className="capitalize">
|
||||||
Select an option or create one
|
{singleValue && tags.size > 0
|
||||||
|
? "Only one option can be selected"
|
||||||
|
: "Select an option or create one"}
|
||||||
</p>
|
</p>
|
||||||
<TagDropdown
|
{(!singleValue || tags.size === 0) && (
|
||||||
handleDeleteTag={handleDeleteTagOption}
|
<>
|
||||||
handleEditTag={handleEditTag}
|
<TagDropdown
|
||||||
handleAdd={handleSelectTag}
|
handleDeleteTag={
|
||||||
tags={options}
|
handleDeleteTagOption
|
||||||
/>
|
}
|
||||||
{inputValue.length > 0 && (
|
handleEditTag={handleEditTag}
|
||||||
<CreateNewTagAction input={inputValue} />
|
handleAdd={handleSelectTag}
|
||||||
|
tags={filteredOptions}
|
||||||
|
/>
|
||||||
|
{inputValue.length > 0 && (
|
||||||
|
<CreateNewTagAction
|
||||||
|
input={inputValue}
|
||||||
|
addTag={addTag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,26 @@
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
export const Tag = ({ children, handleDelete, active = false }) => {
|
interface TagProps {
|
||||||
|
children: string;
|
||||||
|
handleDelete?: (tag: string) => void;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tag = ({ children, handleDelete, active = false }: TagProps) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`}
|
className={`font-normal bg-purple-100 text-gray-800 flex flex-row p-1 px-2 rounded-lg`}
|
||||||
|
style={{ textTransform: "none" }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{active && handleDelete && (
|
{active && handleDelete && (
|
||||||
<button onClick={() => handleDelete(children)}>
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(children);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<XMarkIcon className={`ml-1 w-3 text-purple-500`} />
|
<XMarkIcon className={`ml-1 w-3 text-purple-500`} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,29 +1,42 @@
|
||||||
import { Tag } from "./Tag";
|
import { Tag } from "./Tag";
|
||||||
import { DropdownAction } from "./DropdownAction";
|
import { DropdownAction } from "./DropdownAction";
|
||||||
|
|
||||||
|
interface TagDropdownProps {
|
||||||
|
tags: Set<string>;
|
||||||
|
handleEditTag: (oldTag: string, newTag: string) => void;
|
||||||
|
handleDeleteTag: (tag: string) => void;
|
||||||
|
handleAdd: (tag: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const TagDropdown = ({
|
export const TagDropdown = ({
|
||||||
tags,
|
tags,
|
||||||
handleEditTag,
|
handleEditTag,
|
||||||
handleDeleteTag,
|
handleDeleteTag,
|
||||||
handleAdd,
|
handleAdd,
|
||||||
}) => {
|
}: TagDropdownProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="z-50 flex flex-col space-y-2 mt-2">
|
<div className="z-50 flex flex-col space-y-2 mt-2 max-h-60 overflow-y-auto scrollbar-thin scrollbar-track-gray-100 scrollbar-thumb-gray-300 pr-2">
|
||||||
{Array.from(tags).map((tag, index) => (
|
{Array.from(tags).length > 0 ? (
|
||||||
<div
|
Array.from(tags).map((tag, index) => (
|
||||||
key={index}
|
<div
|
||||||
className="items-center rounded-md p-1 flex flex-row justify-between hover:bg-gray-100"
|
key={index}
|
||||||
>
|
className="items-center rounded-md p-1 flex flex-row justify-between hover:bg-gray-100"
|
||||||
<button onClick={() => handleAdd(tag)}>
|
>
|
||||||
<Tag>{tag}</Tag>
|
<button onClick={() => handleAdd(tag)}>
|
||||||
</button>
|
<Tag>{tag}</Tag>
|
||||||
<DropdownAction
|
</button>
|
||||||
handleDeleteTag={handleDeleteTag}
|
<DropdownAction
|
||||||
handleEditTag={handleEditTag}
|
handleDeleteTag={handleDeleteTag}
|
||||||
tag={tag}
|
handleEditTag={handleEditTag}
|
||||||
/>
|
tag={tag}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-500 text-sm p-1">
|
||||||
|
No options available. Type to create new ones.
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,21 +7,25 @@ export interface Tags {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
|
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
|
||||||
// console.log(tags);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex ml-2 flex-wrap gap-2 items-center">
|
<div className="flex flex-wrap gap-2 items-center min-h-[24px] min-w-[100px] rounded-md hover:bg-gray-100 p-1">
|
||||||
{Array.from(tags).map((tag, index) => {
|
{Array.from(tags).length > 0 ? (
|
||||||
return (
|
Array.from(tags).map((tag, index) => {
|
||||||
<Tag
|
return (
|
||||||
handleDelete={handleDelete}
|
<Tag
|
||||||
active={active}
|
handleDelete={handleDelete}
|
||||||
key={index}
|
active={active}
|
||||||
>
|
key={index}
|
||||||
{tag}
|
>
|
||||||
</Tag>
|
{tag}
|
||||||
);
|
</Tag>
|
||||||
})}
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 text-sm cursor-pointer">
|
||||||
|
Click to select tags
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,9 +11,15 @@ const Loading = () => {
|
||||||
alt="Compass Center logo."
|
alt="Compass Center logo."
|
||||||
width={100}
|
width={100}
|
||||||
height={91}
|
height={91}
|
||||||
|
style={{ height: "auto", width: "auto" }}
|
||||||
|
priority
|
||||||
/>
|
/>
|
||||||
<h1 className={styles.loadingTitle}>Loading...</h1>
|
<h1 className="text-2xl font-semibold text-gray-700 mt-4 mb-6">
|
||||||
<div className={styles.loadingSpinner}></div>
|
Loading...
|
||||||
|
</h1>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-24 w-24 border-b-2 border-gray-700"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
24
compass/components/auth/LoggingOut.tsx
Normal file
24
compass/components/auth/LoggingOut.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// components/LoggingOut.js
|
||||||
|
import styles from "./Loading.module.css";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
const LoggingOut = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.loadingOverlay}>
|
||||||
|
<div className={styles.loadingContent}>
|
||||||
|
<Image
|
||||||
|
src="/logo.png"
|
||||||
|
alt="Compass Center logo."
|
||||||
|
width={100}
|
||||||
|
height={91}
|
||||||
|
style={{ height: "auto", width: "auto" }}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<h1 className={styles.loadingTitle}>Signing out...</h1>
|
||||||
|
<div className={styles.loadingSpinner}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoggingOut;
|
|
@ -1,247 +0,0 @@
|
||||||
import { FunctionComponent, ReactNode } from "react";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
|
|
||||||
import {
|
|
||||||
StarIcon as SolidStarIcon,
|
|
||||||
EnvelopeIcon,
|
|
||||||
UserIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
import {
|
|
||||||
ArrowsPointingOutIcon,
|
|
||||||
ArrowsPointingInIcon,
|
|
||||||
StarIcon as OutlineStarIcon,
|
|
||||||
ListBulletIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import TagsInput from "../TagsInput/Index";
|
|
||||||
|
|
||||||
type DrawerProps = {
|
|
||||||
title: string;
|
|
||||||
children: ReactNode;
|
|
||||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
||||||
type?: "button" | "submit" | "reset"; // specify possible values for type
|
|
||||||
disabled?: boolean;
|
|
||||||
editableContent?: any;
|
|
||||||
onSave?: (content: any) => void;
|
|
||||||
rowContent?: any;
|
|
||||||
onRowUpdate?: (content: any) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EditContent {
|
|
||||||
content: string;
|
|
||||||
isEditing: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Drawer: FunctionComponent<DrawerProps> = ({
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
onSave,
|
|
||||||
editableContent,
|
|
||||||
rowContent,
|
|
||||||
onRowUpdate,
|
|
||||||
}) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isFull, setIsFull] = useState(false);
|
|
||||||
const [isFavorite, setIsFavorite] = useState(false);
|
|
||||||
const [tempRowContent, setTempRowContent] = useState(rowContent);
|
|
||||||
|
|
||||||
const handleTempRowContentChange = (e) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
console.log(name);
|
|
||||||
console.log(value);
|
|
||||||
setTempRowContent((prevContent) => ({
|
|
||||||
...prevContent,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnterPress = (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
// Update the rowContent with the temporaryRowContent
|
|
||||||
if (onRowUpdate) {
|
|
||||||
onRowUpdate(tempRowContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDrawer = () => {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
if (isFull) {
|
|
||||||
setIsFull(!isFull);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDrawerFullScreen = () => setIsFull(!isFull);
|
|
||||||
|
|
||||||
const toggleFavorite = () => setIsFavorite(!isFavorite);
|
|
||||||
|
|
||||||
const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${
|
|
||||||
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
|
|
||||||
} ${isFull ? "w-full" : "w-1/2"}`;
|
|
||||||
|
|
||||||
const iconComponent = isFull ? (
|
|
||||||
<ArrowsPointingInIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<ArrowsPointingOutIcon className="h-5 w-5" />
|
|
||||||
);
|
|
||||||
|
|
||||||
const favoriteIcon = isFavorite ? (
|
|
||||||
<SolidStarIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<OutlineStarIcon className="h-5 w-5" />
|
|
||||||
);
|
|
||||||
|
|
||||||
const [presetOptions, setPresetOptions] = useState([
|
|
||||||
"administrator",
|
|
||||||
"volunteer",
|
|
||||||
"employee",
|
|
||||||
]);
|
|
||||||
const [rolePresetOptions, setRolePresetOptions] = useState([
|
|
||||||
"domestic",
|
|
||||||
"community",
|
|
||||||
"economic",
|
|
||||||
]);
|
|
||||||
const [tagColors, setTagColors] = useState(new Map());
|
|
||||||
|
|
||||||
const getTagColor = (tag: string) => {
|
|
||||||
if (!tagColors.has(tag)) {
|
|
||||||
const colors = [
|
|
||||||
"bg-cyan-100",
|
|
||||||
"bg-blue-100",
|
|
||||||
"bg-green-100",
|
|
||||||
"bg-yellow-100",
|
|
||||||
"bg-purple-100",
|
|
||||||
];
|
|
||||||
const randomColor =
|
|
||||||
colors[Math.floor(Math.random() * colors.length)];
|
|
||||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
|
||||||
}
|
|
||||||
return tagColors.get(tag);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
"ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md"
|
|
||||||
}
|
|
||||||
onClick={toggleDrawer}
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</button>
|
|
||||||
<div className={drawerClassName}></div>
|
|
||||||
<div className={drawerClassName}>
|
|
||||||
<div className="flex items-center justify-between p-4">
|
|
||||||
<div className="flex flex-row items-center justify-between space-x-2">
|
|
||||||
<span className="h-5 text-purple-200 w-5">
|
|
||||||
<UserIcon />
|
|
||||||
</span>
|
|
||||||
<h2 className="text-lg text-gray-800 font-semibold">
|
|
||||||
{rowContent.username}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={toggleFavorite}
|
|
||||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
|
||||||
>
|
|
||||||
{favoriteIcon}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={toggleDrawerFullScreen}
|
|
||||||
className="py-2 text-gray-500 hover:text-gray-800 mr-2"
|
|
||||||
>
|
|
||||||
{iconComponent}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={toggleDrawer}
|
|
||||||
className="py-2 text-gray-500 hover:text-gray-800"
|
|
||||||
>
|
|
||||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<table className="p-4">
|
|
||||||
<tbody className="items-center">
|
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
|
||||||
<td>
|
|
||||||
<UserIcon className="h-4 w-4" />
|
|
||||||
</td>
|
|
||||||
<td className="w-32">Username</td>
|
|
||||||
</div>
|
|
||||||
<td className="w-3/4 w-3/4 p-2 pl-0">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
value={tempRowContent.username}
|
|
||||||
onChange={handleTempRowContentChange}
|
|
||||||
onKeyDown={handleEnterPress}
|
|
||||||
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
|
||||||
<td>
|
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
|
||||||
</td>
|
|
||||||
<td className="w-32">Role</td>
|
|
||||||
</div>
|
|
||||||
<td className="w-3/4 hover:bg-gray-50">
|
|
||||||
<TagsInput
|
|
||||||
presetValue={tempRowContent.role}
|
|
||||||
presetOptions={presetOptions}
|
|
||||||
setPresetOptions={setPresetOptions}
|
|
||||||
getTagColor={getTagColor}
|
|
||||||
setTagColors={setTagColors}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
|
||||||
<td>
|
|
||||||
<EnvelopeIcon className="h-4 w-4" />
|
|
||||||
</td>
|
|
||||||
<td className="w-32">Email</td>
|
|
||||||
</div>
|
|
||||||
<td className="w-3/4 p-2 pl-0">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="email"
|
|
||||||
value={tempRowContent.email}
|
|
||||||
onChange={handleTempRowContentChange}
|
|
||||||
onKeyDown={handleEnterPress}
|
|
||||||
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="w-full text-xs items-center flex flex-row justify-between">
|
|
||||||
<div className="flex flex-row space-x-2 text-gray-500 items-center">
|
|
||||||
<td>
|
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
|
||||||
</td>
|
|
||||||
<td className="w-32">Type of Program</td>
|
|
||||||
</div>
|
|
||||||
<td className="w-3/4 hover:bg-gray-50">
|
|
||||||
{/* {rowContent.program} */}
|
|
||||||
<TagsInput
|
|
||||||
presetValue={tempRowContent.program}
|
|
||||||
presetOptions={rolePresetOptions}
|
|
||||||
setPresetOptions={setRolePresetOptions}
|
|
||||||
getTagColor={getTagColor}
|
|
||||||
setTagColors={setTagColors}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Drawer;
|
|
|
@ -23,8 +23,8 @@ export const SearchResult: React.FC<SearchResultProps> = ({
|
||||||
type === "resource"
|
type === "resource"
|
||||||
? BookmarkIcon
|
? BookmarkIcon
|
||||||
: type === "service"
|
: type === "service"
|
||||||
? ClipboardIcon
|
? ClipboardIcon
|
||||||
: QuestionMarkCircleIcon; // Unknown type
|
: QuestionMarkCircleIcon; // Unknown type
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center w-full p-2 rounded-md hover:bg-purple-100 cursor-pointer group">
|
<div className="flex justify-between items-center w-full p-2 rounded-md hover:bg-purple-100 cursor-pointer group">
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { signOut } from "@/app/auth/actions";
|
import { signOut } from "@/app/auth/actions";
|
||||||
|
|
||||||
interface UserProfileProps {
|
interface UserProfileProps {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = async (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement>,
|
||||||
|
setLoading: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
await signOut();
|
await signOut();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserProfile = ({ name, email }: UserProfileProps) => {
|
export const UserProfile = ({ name, email, setLoading }: UserProfileProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-start space-y-2">
|
<div className="flex flex-col items-start space-y-2">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
@ -19,7 +25,7 @@ export const UserProfile = ({ name, email }: UserProfileProps) => {
|
||||||
<span className="text-xs text-gray-500">{email}</span>
|
<span className="text-xs text-gray-500">{email}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={(event) => handleClick(event, setLoading)}
|
||||||
className="text-red-600 font-semibold text-xs hover:underline mt-1"
|
className="text-red-600 font-semibold text-xs hover:underline mt-1"
|
||||||
>
|
>
|
||||||
Sign out
|
Sign out
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"name": "example name",
|
"name": "example name",
|
||||||
"description": "example description"
|
"description": "example description"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "service",
|
"type": "service",
|
||||||
"name": "example name",
|
"name": "example name",
|
||||||
"description": "example description"
|
"description": "example description"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"name": "National Domestic Violence Hotline",
|
"name": "National Domestic Violence Hotline",
|
||||||
"description": "24/7 confidential support for victims of domestic violence"
|
"description": "24/7 confidential support for victims of domestic violence"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"name": "Legal Aid Society",
|
"name": "Legal Aid Society",
|
||||||
"description": "Free legal assistance for low-income individuals"
|
"description": "Free legal assistance for low-income individuals"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "service",
|
"type": "service",
|
||||||
"name": "Crisis Hotline",
|
"name": "Crisis Hotline",
|
||||||
"description": "24/7 support for individuals in crisis"
|
"description": "24/7 support for individuals in crisis"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "unknown",
|
"type": "unknown",
|
||||||
"name": "unknown thing with a really long name",
|
"name": "unknown thing with a really long name",
|
||||||
"description": "and let's also type out a really long description to see how it handles overflow and all that anyways"
|
"description": "and let's also type out a really long description to see how it handles overflow and all that anyways"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,7 +21,7 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [require("tailwind-scrollbar")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||||
import { NextResponse, type NextRequest } from "next/server";
|
import { User } from "@supabase/supabase-js";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { Role } from "../models/User";
|
||||||
|
|
||||||
export async function updateSession(request: NextRequest) {
|
export async function updateSession(request: NextRequest) {
|
||||||
let response = NextResponse.next({
|
let response = NextResponse.next({
|
||||||
|
@ -54,7 +56,50 @@ export async function updateSession(request: NextRequest) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await supabase.auth.getUser();
|
const { data, error } = await supabase.auth.getUser();
|
||||||
|
|
||||||
|
const authenticatedRoutes = ["/admin", "/resource", "/home", "/service"];
|
||||||
|
const pathname = request.nextUrl.pathname;
|
||||||
|
|
||||||
|
for (const route of authenticatedRoutes) {
|
||||||
|
if (error && pathname.startsWith(route)) {
|
||||||
|
console.log("redirected");
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL(
|
||||||
|
"/auth/login",
|
||||||
|
request.nextUrl.protocol + "//" + request.nextUrl.host
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith("/admin") && data.user) {
|
||||||
|
// After the previous checks we can assume the user is not empty
|
||||||
|
const userData = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_HOST}/api/user?uuid=${data.user.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const user: User = await userData.json();
|
||||||
|
|
||||||
|
if (user.role !== Role.ADMIN) {
|
||||||
|
console.log("redirected as not admin");
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL(
|
||||||
|
"/home",
|
||||||
|
request.nextUrl.protocol + "//" + request.nextUrl.host
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.user && pathname.startsWith("/auth/login")) {
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL(
|
||||||
|
"/home",
|
||||||
|
request.nextUrl.protocol + "//" + request.nextUrl.host
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
26
package-lock.json
generated
26
package-lock.json
generated
|
@ -1,12 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "compass",
|
"name": "workspace",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"lint-staged": "^15.2.2"
|
"lint-staged": "^15.2.2",
|
||||||
|
"tailwind-scrollbar": "^4.0.0-beta.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-escapes": {
|
"node_modules/ansi-escapes": {
|
||||||
|
@ -649,6 +650,27 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-scrollbar": {
|
||||||
|
"version": "4.0.0-beta.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-4.0.0-beta.0.tgz",
|
||||||
|
"integrity": "sha512-d6qwt3rYDgsKNaQGLW0P6N1TN/87xYZDjH6/PimtFvij2NgC5i3M6mEuVKR4Ixb2u3SvMBT95t7+xzJGJRzXtA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": "^4.0.0-beta.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tailwindcss": {
|
||||||
|
"version": "4.0.0-beta.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0-beta.8.tgz",
|
||||||
|
"integrity": "sha512-21HmdRq9tHDLJZavb2cRBGJxBvRODpwb0/t3tRbMOl65hJE6zG6K6lD6lLS3IOC35u4SOjKjdZiJJi9AuWCf+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|
33
package.json
33
package.json
|
@ -1,21 +1,22 @@
|
||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"lint-staged": "^15.2.2"
|
"lint-staged": "^15.2.2",
|
||||||
|
"tailwind-scrollbar": "^4.0.0-beta.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"precommit": "lint-staged"
|
"precommit": "lint-staged"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.ts": [
|
"*.ts": [
|
||||||
"cd compass",
|
"cd compass",
|
||||||
"prettier --write",
|
"prettier --write",
|
||||||
"git add"
|
"git add"
|
||||||
],
|
],
|
||||||
"*.tsx": [
|
"*.tsx": [
|
||||||
"cd compass",
|
"cd compass",
|
||||||
"prettier --write",
|
"prettier --write",
|
||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user