Skip to content

Commit e24cd8f

Browse files
committed
chore: replace magic numbers by functional values,
standardise api call with fetchApi instead of fetch + injected url, refactor Modal usage with custom hook
1 parent a358c65 commit e24cd8f

File tree

13 files changed

+120
-143
lines changed

13 files changed

+120
-143
lines changed

webapp/src/app/(dashboard)/[organizationId]/members/members-list.tsx

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useRouter } from "next/navigation";
1010
import { useState } from "react";
1111
import { z } from "zod";
1212
import { toast } from "sonner";
13+
import { fetchApi } from "@/utils/api";
1314

1415
export default function MembersList({
1516
users,
@@ -44,36 +45,17 @@ export default function MembersList({
4445

4546
await toast
4647
.promise(
47-
fetch(
48-
`${process.env.NEXT_PUBLIC_API_URL}/organizations/${organizationId}/add-user`,
48+
fetchApi(
49+
`/organizations/${organizationId}/add-user`,
4950
{
5051
method: "POST",
51-
headers: {
52-
Accept: "application/json",
53-
"Content-Type": "application/json",
54-
},
5552
body: body,
5653
},
5754
).then(async (result) => {
58-
const data = await result.json();
59-
if (result.status !== 200) {
60-
const errorObject = data.detail;
61-
let errorMessage = "Failed to add user";
62-
63-
if (
64-
Array.isArray(errorObject) &&
65-
errorObject.length > 0
66-
) {
67-
errorMessage = errorObject
68-
.map((error: any) => error.msg)
69-
.join("\n");
70-
} else if (errorObject) {
71-
errorMessage = JSON.stringify(errorObject);
72-
}
73-
74-
throw new Error(errorMessage);
55+
if (!result) {
56+
throw new Error("Failed to add user");
7557
}
76-
return data;
58+
return result;
7759
}),
7860
{
7961
loading: `Adding user ${email}...`,

webapp/src/app/(dashboard)/[organizationId]/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getEquivalentCitizenPercentage,
1212
getEquivalentTvTime,
1313
} from "@/helpers/constants";
14+
import { REFRESH_INTERVAL_ONE_MINUTE, THIRTY_DAYS_MS, SECONDS_PER_DAY } from "@/helpers/time-constants";
1415
import { fetcher } from "@/helpers/swr";
1516
import { getOrganizationEmissionsByProject } from "@/server-functions/organizations";
1617
import { Organization } from "@/types/organization";
@@ -29,12 +30,12 @@ export default function OrganizationPage({
2930
isLoading,
3031
error,
3132
} = useSWR<Organization>(`/organizations/${organizationId}`, fetcher, {
32-
refreshInterval: 1000 * 60, // Refresh every minute
33+
refreshInterval: REFRESH_INTERVAL_ONE_MINUTE,
3334
});
3435

3536
const today = new Date();
3637
const [date, setDate] = useState<DateRange | undefined>({
37-
from: new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000),
38+
from: new Date(today.getTime() - THIRTY_DAYS_MS),
3839
to: today,
3940
});
4041
const [organizationReport, setOrganizationReport] = useState<
@@ -86,7 +87,7 @@ export default function OrganizationPage({
8687
label: "days",
8788
value: organizationReport?.duration
8889
? parseFloat(
89-
(organizationReport.duration / 86400, 0).toFixed(2),
90+
(organizationReport.duration / SECONDS_PER_DAY).toFixed(2),
9091
)
9192
: 0,
9293
},

webapp/src/app/(dashboard)/[organizationId]/projects/page.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
1010
import { Card } from "@/components/ui/card";
1111
import { Table, TableBody } from "@/components/ui/table";
1212
import { fetcher } from "@/helpers/swr";
13+
import { REFRESH_INTERVAL_ONE_MINUTE } from "@/helpers/time-constants";
14+
import { useModal } from "@/hooks/useModal";
1315
import { getProjects, deleteProject } from "@/server-functions/projects";
1416
import { Project } from "@/types/project";
1517
import { use, useEffect, useState } from "react";
@@ -22,15 +24,15 @@ export default function ProjectsPage({
2224
params: Promise<{ organizationId: string }>;
2325
}) {
2426
const { organizationId } = use(params);
25-
const [isModalOpen, setIsModalOpen] = useState(false);
27+
const createModal = useModal();
28+
const deleteModal = useModal();
2629
const [projectList, setProjectList] = useState<Project[]>([]);
27-
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
2830
const [projectToDelete, setProjectToDelete] = useState<Project | null>(
2931
null,
3032
);
3133

3234
const handleClick = async () => {
33-
setIsModalOpen(true);
35+
createModal.open();
3436
};
3537

3638
const refreshProjectList = async () => {
@@ -41,7 +43,7 @@ export default function ProjectsPage({
4143

4244
const handleDeleteClick = (project: Project) => {
4345
setProjectToDelete(project);
44-
setDeleteModalOpen(true);
46+
deleteModal.open();
4547
};
4648

4749
const handleDeleteConfirm = async (projectId: string) => {
@@ -61,7 +63,7 @@ export default function ProjectsPage({
6163
error,
6264
isLoading,
6365
} = useSWR<Project[]>(`/projects?organization=${organizationId}`, fetcher, {
64-
refreshInterval: 1000 * 60, // Refresh every minute
66+
refreshInterval: REFRESH_INTERVAL_ONE_MINUTE,
6567
});
6668

6769
useEffect(() => {
@@ -104,8 +106,8 @@ export default function ProjectsPage({
104106
</Button>
105107
<CreateProjectModal
106108
organizationId={organizationId}
107-
isOpen={isModalOpen}
108-
onClose={() => setIsModalOpen(false)}
109+
isOpen={createModal.isOpen}
110+
onClose={createModal.close}
109111
onProjectCreated={refreshProjectList}
110112
/>
111113
</div>
@@ -141,8 +143,8 @@ export default function ProjectsPage({
141143
</Card>
142144
{projectToDelete && (
143145
<DeleteProjectModal
144-
open={deleteModalOpen}
145-
onOpenChange={setDeleteModalOpen}
146+
open={deleteModal.isOpen}
147+
onOpenChange={deleteModal.setIsOpen}
146148
projectName={projectToDelete.name}
147149
projectId={projectToDelete.id}
148150
onDelete={handleDeleteConfirm}

webapp/src/app/(dashboard)/profile/page.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,15 @@ import { Input } from "@/components/ui/input";
44
import { Label } from "@/components/ui/label";
55
import { fiefAuth } from "@/helpers/fief";
66
import { User } from "@/types/user";
7+
import { fetchApiServer } from "@/helpers/api-server";
78

89
async function getUser(): Promise<User | null> {
910
const userId = await fiefAuth.getUserId();
1011
if (!userId) {
1112
return null;
1213
}
13-
const res = await fetch(
14-
`${process.env.NEXT_PUBLIC_API_URL}/users/${userId}`,
15-
);
16-
17-
if (!res.ok) {
18-
// This will activate the closest `error.js` Error Boundary
19-
console.error("Failed to fetch user", res.statusText);
20-
return null;
21-
}
2214

23-
return res.json();
15+
return await fetchApiServer<User>(`/users/${userId}`);
2416
}
2517

2618
export default async function ProfilePage() {

webapp/src/components/navbar.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import CreateOrganizationModal from "./createOrganizationModal";
2626
import { getOrganizations } from "@/server-functions/organizations";
2727
import { Button } from "./ui/button";
28+
import { useModal } from "@/hooks/useModal";
2829

2930
const USER_PROFILE_URL = process.env.NEXT_PUBLIC_FIEF_BASE_URL; // Redirect to Fief profile to handle profile updates there
3031
export default function NavBar({
@@ -40,7 +41,7 @@ export default function NavBar({
4041
const [selectedOrg, setSelectedOrg] = useState<string | null>(null);
4142
const iconStyles = "h-4 w-4 flex-shrink-0 text-muted-foreground";
4243
const pathname = usePathname();
43-
const [isNewOrgModalOpen, setNewOrgModalOpen] = useState(false);
44+
const newOrgModal = useModal();
4445
const [organizationList, setOrganizationList] = useState<
4546
Organization[] | undefined
4647
>([]);
@@ -120,7 +121,7 @@ export default function NavBar({
120121
}, [pathname, organizationList, selectedOrg]);
121122

122123
const handleNewOrgClick = async () => {
123-
setNewOrgModalOpen(true);
124+
newOrgModal.open();
124125
setDropdownOpen(false); // Close the dropdown menu
125126
};
126127

@@ -247,8 +248,8 @@ export default function NavBar({
247248
</Select>
248249
)}
249250
<CreateOrganizationModal
250-
isOpen={isNewOrgModalOpen}
251-
onClose={() => setNewOrgModalOpen(false)}
251+
isOpen={newOrgModal.isOpen}
252+
onClose={newOrgModal.close}
252253
onOrganizationCreated={refreshOrgList}
253254
/>
254255
<NavItem

webapp/src/components/project-dashboard.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { toast } from "sonner";
1818
import ProjectDashboardBase from "./project-dashboard-base";
1919
import ProjectSettingsModal from "./project-settings-modal";
2020
import ShareProjectButton from "./share-project-button";
21+
import { useModal } from "@/hooks/useModal";
2122

2223
export default function ProjectDashboard({
2324
project,
@@ -35,7 +36,7 @@ export default function ProjectDashboard({
3536
onSettingsClick,
3637
isLoading,
3738
}: ProjectDashboardProps) {
38-
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
39+
const settingsModal = useModal();
3940
const [isExporting, setIsExporting] = useState(false);
4041

4142
const handleJsonExport = () => {
@@ -163,7 +164,7 @@ export default function ProjectDashboard({
163164
className="p-1 rounded-full"
164165
variant="outline"
165166
size="icon"
166-
onClick={() => setIsSettingsModalOpen(true)}
167+
onClick={settingsModal.open}
167168
>
168169
<SettingsIcon className="h-5 w-5" />
169170
</Button>
@@ -192,8 +193,8 @@ export default function ProjectDashboard({
192193
/>
193194

194195
<ProjectSettingsModal
195-
open={isSettingsModalOpen}
196-
onOpenChange={setIsSettingsModalOpen}
196+
open={settingsModal.isOpen}
197+
onOpenChange={settingsModal.setIsOpen}
197198
project={project}
198199
onProjectUpdated={() => {
199200
// Call the original onSettingsClick to refresh the data

webapp/src/helpers/dashboard-calculations.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getEquivalentCitizenPercentage,
55
getEquivalentTvTime,
66
} from "./constants";
7+
import { SECONDS_PER_DAY } from "./time-constants";
78

89
export type RadialChartData = {
910
energy: { label: string; value: number };
@@ -44,7 +45,7 @@ export function calculateRadialChartData(
4445
label: "days",
4546
value: parseFloat(
4647
report
47-
.reduce((n, { duration }) => n + duration / 86400, 0)
48+
.reduce((n, { duration }) => n + duration / SECONDS_PER_DAY, 0)
4849
.toFixed(2),
4950
),
5051
},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Time-related constants to avoid magic numbers
3+
*/
4+
5+
// Base time units in milliseconds
6+
export const MILLISECONDS_PER_SECOND = 1000;
7+
export const SECONDS_PER_MINUTE = 60;
8+
export const MINUTES_PER_HOUR = 60;
9+
export const HOURS_PER_DAY = 24;
10+
export const DAYS_PER_WEEK = 7;
11+
export const WEEKS_PER_YEAR = 52;
12+
13+
// Composite time units in milliseconds
14+
export const ONE_SECOND_MS = MILLISECONDS_PER_SECOND;
15+
export const ONE_MINUTE_MS = ONE_SECOND_MS * SECONDS_PER_MINUTE;
16+
export const ONE_HOUR_MS = ONE_MINUTE_MS * MINUTES_PER_HOUR;
17+
export const ONE_DAY_MS = ONE_HOUR_MS * HOURS_PER_DAY;
18+
export const ONE_WEEK_MS = ONE_DAY_MS * DAYS_PER_WEEK;
19+
20+
// Common time intervals
21+
export const THIRTY_DAYS_MS = 30 * ONE_DAY_MS;
22+
23+
// Seconds conversions
24+
export const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
25+
export const SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;
26+
27+
// Common refresh intervals
28+
export const REFRESH_INTERVAL_ONE_MINUTE = ONE_MINUTE_MS;

webapp/src/hooks/useModal.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useCallback, useState } from "react";
2+
3+
/**
4+
* Custom hook for managing modal open/close state
5+
* Reduces boilerplate for modal state management
6+
*
7+
* @param defaultOpen - Initial open state (default: false)
8+
* @returns Object with isOpen state and open/close/toggle functions
9+
*/
10+
export function useModal(defaultOpen = false) {
11+
const [isOpen, setIsOpen] = useState(defaultOpen);
12+
13+
const open = useCallback(() => setIsOpen(true), []);
14+
const close = useCallback(() => setIsOpen(false), []);
15+
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
16+
17+
return { isOpen, open, close, toggle, setIsOpen };
18+
}

webapp/src/server-functions/experiments.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,26 @@ import { DateRange } from "react-day-picker";
66
export async function createExperiment(
77
experiment: Experiment,
88
): Promise<Experiment> {
9-
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/experiments`, {
9+
const result = await fetchApi<Experiment>("/experiments", {
1010
method: "POST",
11-
headers: {
12-
"Content-Type": "application/json",
13-
},
14-
body: JSON.stringify({
15-
...experiment,
16-
}),
11+
body: JSON.stringify(experiment),
1712
});
1813

19-
if (!res.ok) {
14+
if (!result) {
2015
throw new Error("Failed to create experiment");
2116
}
2217

23-
const result = await res.json();
2418
return result;
2519
}
2620
export async function getExperiments(projectId: string): Promise<Experiment[]> {
27-
const res = await fetch(
28-
`${process.env.NEXT_PUBLIC_API_URL}/projects/${projectId}/experiments`,
21+
const result = await fetchApi<Experiment[]>(
22+
`/projects/${projectId}/experiments`,
2923
);
3024

31-
if (!res.ok) {
32-
throw new Error("Failed to fetch experiments");
25+
if (!result) {
26+
return [];
3327
}
3428

35-
const result = await res.json();
3629
return result.map((experiment: Experiment) => {
3730
return {
3831
id: experiment.id,

0 commit comments

Comments
 (0)