Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
661 changes: 661 additions & 0 deletions docs/Permission-Based-UI-Rendering.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@reduxjs/toolkit": "^2.11.2",
"@tanstack/react-query": "^5.100.9",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-opener": "2.5.4",
Expand All @@ -34,6 +35,7 @@
"react-dom": "19.2.6",
"react-hook-form": "^7.75.0",
"react-icons": "^5.6.0",
"react-redux": "^9.2.0",
"react-router": "^7.15.0",
"react-router-dom": "^7.15.0",
"recharts": "^3.8.1",
Expand Down
10 changes: 3 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useEffect } from "react";

import { BrowserRouter } from "react-router-dom";

import "./App.css";


import { startAutoUpdater } from "./system/updater/autoUpdater";
import { ThemeProvider } from "next-themes";

import { ThemeProvider } from "./theme";
import OrgRoute from "./routes/OrgRoute";
Expand All @@ -20,11 +19,8 @@ function App() {
return (
<ThemeProvider>
<BrowserRouter>

<OrgRoute />

<MemberRoutes />

<OrgRoute />
<MemberRoutes />
</BrowserRouter>
</ThemeProvider>
);
Expand Down
17 changes: 11 additions & 6 deletions src/features/AddMember/v1/Component/AddMemberHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IoMdArrowRoundBack } from "react-icons/io";
import { Member_Permissions, PermissionGate } from "@/permissions";
import { Link } from "react-router";
import Button from "../../../../Component/ui/Button";
import { memo } from "react";
Expand All @@ -20,12 +21,16 @@ const AddMemberHeader = () => {
</Link>

<div className="w-[40%] h-full mr-[3vw] flex justify-end gap-3">
<Button
text="Discard Draft"
variant="secondary"
onClick={() => alert("Discard Draft clicked")}
/>
<Button text="Create Member" onClick={() => alert("Create Member clicked")} />
<PermissionGate permission={Member_Permissions.CREATE_MEMBER}>
<Button
text="Discard Draft"
variant="secondary"
onClick={() => alert("Discard Draft clicked")}
/>
</PermissionGate>
<PermissionGate permission={Member_Permissions.CREATE_MEMBER}>
<Button text="Create Member" onClick={() => alert("Create Member clicked")} />
</PermissionGate>
</div>
</div>
);
Expand Down
26 changes: 19 additions & 7 deletions src/features/AddMember/v1/Page/AddMemberPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AddMemberHeader from "../Component/AddMemberHeader";
import { AccessDenied, Member_Permissions, PermissionBoundary, PermissionLoading } from "@/permissions";
import Administrative_MetaData from "../Component/Administrative_MetaData";
import Community_Involvement from "../Component/Community_Involment";
import PersonalInfoCard from "../Sections/PersonalInfoCard";
Expand All @@ -8,14 +9,25 @@ const AddMemberPage = () => {
return (
<div className="w-full flex flex-col cd-page">
<AddMemberHeader />
<div className="flex p-8 gap-8 w-full flex-col lg:flex-row items-start overflow-x-hidden">
<div className="w-full lg:w-[65%] h-full flex-col">
<PersonalInfoCard />
<ProfessionalDetails />
<Community_Involvement />
<PermissionBoundary
permission={Member_Permissions.CREATE_MEMBER}
loadingFallback={<PermissionLoading />}
unauthorizedFallback={
<AccessDenied
title="You cannot create members yet"
description="Member creation tools are only visible to users with create access for the member directory."
/>
}
>
<div className="flex p-8 gap-8 w-full flex-col lg:flex-row items-start overflow-x-hidden">
<div className="w-full lg:w-[65%] h-full flex-col">
<PersonalInfoCard />
<ProfessionalDetails />
<Community_Involvement />
</div>
<Administrative_MetaData />
</div>
<Administrative_MetaData />
</div>
</PermissionBoundary>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import { Contact_Permissions, usePermissionMap } from "@/permissions";
import { FiMail, FiCopy, FiCheck } from "react-icons/fi";

type TeamMember = {
Expand Down Expand Up @@ -94,6 +95,10 @@ const CopyEmailButton = ({ email }: { email: string }) => {
};

const InternalSupport_Table = () => {
const { canEmail } = usePermissionMap({
canEmail: Contact_Permissions.EMAIL_CONTACT,
});

return (
<div className="w-full h-fit overflow-x-auto">
<table className="cd-table min-w-[680px]">
Expand All @@ -102,7 +107,7 @@ const InternalSupport_Table = () => {
<th>Team Member</th>
<th>Role / Department</th>
<th>Email Address</th>
<th className="text-center">Actions</th>
<th className="text-center">{canEmail ? "Actions" : "Quick Copy"}</th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -148,21 +153,24 @@ const InternalSupport_Table = () => {
</td>
<td>
<div className="flex items-center justify-center gap-1">
<a
href={`mailto:${member.email}`}
title="Send email"
className="p-1.5 rounded-md transition-colors"
style={{ color: "var(--cd-text-muted)" }}
onMouseEnter={(e) =>
((e.currentTarget as HTMLAnchorElement).style.backgroundColor =
"var(--cd-primary-subtle)")
}
onMouseLeave={(e) =>
((e.currentTarget as HTMLAnchorElement).style.backgroundColor = "transparent")
}
>
<FiMail size={14} />
</a>
{canEmail && (
<a
href={`mailto:${member.email}`}
title="Send email"
className="p-1.5 rounded-md transition-colors"
style={{ color: "var(--cd-text-muted)" }}
onMouseEnter={(e) =>
((e.currentTarget as HTMLAnchorElement).style.backgroundColor =
"var(--cd-primary-subtle)")
}
onMouseLeave={(e) =>
((e.currentTarget as HTMLAnchorElement).style.backgroundColor =
"transparent")
}
>
<FiMail size={14} />
</a>
)}
<CopyEmailButton email={member.email} />
</div>
</td>
Expand Down
27 changes: 22 additions & 5 deletions src/features/Contact_And_Support/v1/Components/Support.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import DropDown from "@/Component/ui/DropDown";
import Input from "@/Component/ui/Input";
import { Contact_Permissions, usePermissionMap } from "@/permissions";
import { FormEvent, useState } from "react";
import { MdOutlineSupportAgent } from "react-icons/md";
import { FiAlertCircle, FiCheckCircle } from "react-icons/fi";
Expand All @@ -22,6 +23,9 @@ const priorityColor: Record<PriorityLevel, string> = {
};

const Support = () => {
const { canSubmitTicket } = usePermissionMap({
canSubmitTicket: Contact_Permissions.SUBMIT_SUPPORT_TICKET,
});
const initialCategory = CONTACT_AND_SUPPORT_CONSTANT[0] ?? "";
const [selectedCategory, setSelectedCategory] = useState<string>(initialCategory);
const [priority, setPriority] = useState<PriorityLevel>("Medium");
Expand Down Expand Up @@ -56,6 +60,7 @@ const Support = () => {

const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!canSubmitTicket) return;
if (!validateForm()) {
setTicketReference("");
return;
Expand Down Expand Up @@ -191,11 +196,18 @@ const Support = () => {
)}

<div className="flex flex-wrap items-center gap-3 pt-1">
<button type="submit" className="cd-btn cd-btn-primary">
Submit Ticket
</button>
<button type="button" onClick={clearAll} className="cd-btn cd-btn-secondary">
Clear Form
{canSubmitTicket && (
<button type="submit" className="cd-btn cd-btn-primary">
Submit Ticket
</button>
)}
<button
type="button"
onClick={canSubmitTicket ? clearAll : undefined}
className="cd-btn cd-btn-secondary"
disabled={!canSubmitTicket}
>
{canSubmitTicket ? "Clear Form" : "View Only"}
</button>
</div>

Expand All @@ -214,6 +226,11 @@ const Support = () => {
turnaround time.
</p>
</div>
{!canSubmitTicket && (
<p className="text-xs" style={{ color: "var(--cd-text-muted)" }}>
Ticket submission is hidden until support-request permission is granted.
</p>
)}
</form>
</div>
);
Expand Down
26 changes: 19 additions & 7 deletions src/features/Contact_And_Support/v1/Pages/Contact.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import ContactHeader from "../Section/ContactHeader";
import { AccessDenied, Contact_Permissions, PermissionBoundary, PermissionLoading } from "@/permissions";
import InternalDirectoryTable from "../Section/InternalDirectoryTable";
import Support from "../Components/Support";

const Contact = () => {
return (
<div className="w-full h-full min-h-0 flex flex-col cd-page">
<ContactHeader />
<div className="w-full min-h-0 flex flex-col xl:flex-row p-4 sm:p-5 lg:p-6 gap-4 lg:gap-6">
<div className="w-full min-w-0 xl:flex-1">
<InternalDirectoryTable />
<PermissionBoundary
permission={Contact_Permissions.VIEW_CONTACT_DIRECTORY}
loadingFallback={<PermissionLoading />}
unauthorizedFallback={
<AccessDenied
title="Contact directory access is unavailable"
description="This area is only shown to users who can view the internal support directory."
/>
}
>
<div className="w-full min-h-0 flex flex-col xl:flex-row p-4 sm:p-5 lg:p-6 gap-4 lg:gap-6">
<div className="w-full min-w-0 xl:flex-1">
<InternalDirectoryTable />
</div>
<div className="w-full min-w-0 xl:max-w-[30rem] flex">
<Support />
</div>
</div>
<div className="w-full min-w-0 xl:max-w-[30rem] flex">
<Support />
</div>
</div>
</PermissionBoundary>
</div>
);
};
Expand Down
46 changes: 29 additions & 17 deletions src/features/Events/v1/Components/EventTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MoreVertical } from "lucide-react";
import { useState } from "react";
import { Event_Permissions, usePermissionMap } from "@/permissions";
import { Event } from "../Event.type";

type EventProps = {
Expand All @@ -14,11 +14,18 @@ const statusConfig: Record<Event["status"], { bg: string; color: string }> = {
};

function EventTable({ events, itemsPerPage }: EventProps) {
const { canView, canEdit, canDelete, canPublish } = usePermissionMap({
canView: Event_Permissions.VIEW_EVENT,
canEdit: Event_Permissions.UPDATE_EVENT,
canDelete: Event_Permissions.DELETE_EVENT,
canPublish: Event_Permissions.PUBLISH_EVENT,
});
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(events.length / itemsPerPage);
const indexOfLast = currentPage * itemsPerPage;
const indexOfFirst = indexOfLast - itemsPerPage;
const currentItems = events.slice(indexOfFirst, indexOfLast);
const canManageActions = canView || canEdit || canDelete || canPublish;

return (
<div
Expand All @@ -37,7 +44,7 @@ function EventTable({ events, itemsPerPage }: EventProps) {
<th>Status</th>
<th>Teams</th>
<th>Submissions</th>
<th className="text-right">Actions</th>
{canManageActions && <th className="text-right">Actions</th>}
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -74,21 +81,26 @@ function EventTable({ events, itemsPerPage }: EventProps) {
</td>
<td style={{ color: "var(--cd-text-2)" }}>{event.teams}</td>
<td style={{ color: "var(--cd-text-2)" }}>{event.submissions}</td>
<td className="text-right">
<button
className="p-1.5 rounded-lg transition-colors"
style={{ color: "var(--cd-text-2)" }}
onMouseEnter={(e) =>
((e.currentTarget as HTMLButtonElement).style.backgroundColor =
"var(--cd-hover)")
}
onMouseLeave={(e) =>
((e.currentTarget as HTMLButtonElement).style.backgroundColor = "transparent")
}
>
<MoreVertical size={15} />
</button>
</td>
{canManageActions && (
<td className="text-right">
<div className="flex flex-wrap justify-end gap-2">
{canView && (
<button className="cd-btn cd-btn-secondary px-3 py-1 text-xs">View</button>
)}
{canEdit && (
<button className="cd-btn cd-btn-secondary px-3 py-1 text-xs">Edit</button>
)}
{canPublish && event.status !== "Completed" && (
<button className="cd-btn cd-btn-primary px-3 py-1 text-xs">
Publish
</button>
)}
{canDelete && (
<button className="cd-btn cd-btn-danger px-3 py-1 text-xs">Delete</button>
)}
</div>
</td>
)}
</tr>
);
})}
Expand Down
9 changes: 6 additions & 3 deletions src/features/Events/v1/Components/Judge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import Input from "@/Component/ui/Input";
import { Event_Permissions, PermissionGate } from "@/permissions";
import { CiSearch } from "react-icons/ci";
import { IoMdAdd } from "react-icons/io";
import JudgeCard from "./JudgeCard";
Expand Down Expand Up @@ -53,9 +54,11 @@ const Judge = ({ isExpanded = true, onToggleExpand }: JudgeProps) => {
</span>
</div>
<div className="flex items-center gap-2">
<button type="button" className="cd-btn cd-btn-primary px-2.5 py-1.5 text-xs">
<IoMdAdd className="text-base" /> Add
</button>
<PermissionGate permission={Event_Permissions.UPDATE_EVENT}>
<button type="button" className="cd-btn cd-btn-primary px-2.5 py-1.5 text-xs">
<IoMdAdd className="text-base" /> Add
</button>
</PermissionGate>
{onToggleExpand && (
<button
type="button"
Expand Down
Loading