Skip to content
Merged
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,26 @@ The project is built to demonstrate end-to-end product engineering: system desig
└─────────────────────────────────────────┘
```

```
GATELOG

Authentication
Authorization
┌──────────────────┼──────────────────┐
▼ ▼ ▼
Users Visitors Sites
│ │
▼ ▼
Zones Dashboard
│ │
└──────────┬───────┘
Reports
```

---

## Data Model
Expand Down
1 change: 0 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
services:
postgres:
image: postgres:16-alpine
container_name: gatelog
environment:
POSTGRES_DB: gatelog
POSTGRES_USER: postgres
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const apiClient = axios.create({
timeout: 15_000,
});

// ── Request interceptor inject access token ──────────────────────────────
// Request interceptor - inject access token
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = tokenStorage.getAccess();
Expand All @@ -21,7 +21,7 @@ apiClient.interceptors.request.use(
(error) => Promise.reject(error)
);

// ── Response interceptor silent token refresh on 401 ────────────────────
// Response interceptor - silent token refresh on 401
let isRefreshing = false;
let refreshQueue: Array<{
resolve: (token: string) => void;
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/features/admin/adminApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { apiClient } from "../../api/client"

export interface Site {
id: string,
name: string,
location: string
}

export interface SiteRequest {
name: string,
location: string
}

export interface AdminStats {
totalSites: number,
totalUsers: number,
totalVisitsToday: number,
totalVisitsAllTime: number,
currentlyOnPremises: number
}

export const sitesApi = {
getAll: () => apiClient.get<Site[]>('/api/sites'),
create: (data: SiteRequest) => apiClient.post<Site>('/api/sites', data),
update: (id: string, data: SiteRequest) => apiClient.put<Site>(`/api/sites/${id}`, data),
deactivate: (id: string) => apiClient.delete<void>(`/api/sites/${id}`)
}
63 changes: 63 additions & 0 deletions frontend/src/features/admin/useSites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { sitesApi, type Site, type SiteRequest } from "./adminApi";

interface UseSitesResponse {
sites: Site[],
isLoading: boolean,
error: string | null,
createSite: (data: SiteRequest) => Promise<void>,
updateSite: (id: string, data: SiteRequest) => Promise<void>,
deactivateSite: (id: string) => Promise<void>,
refresh: () => Promise<void>
}

export function useSites(): UseSitesResponse {
const [sites, setSites] = useState<Site[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const isMountedRef = useRef(true)

const fetchSites = useRef(async () => {
setIsLoading(true)
try {
const { data } = await sitesApi.getAll()
if (!isMountedRef.current) return
setSites(data)
setError(null)
} catch {
if (!isMountedRef.current) return
setError('Failed to load sites')
} finally {
if (isMountedRef.current) setIsLoading(false)
}
}).current

useEffect(() => {
isMountedRef.current = true
fetchSites()
return () => { isMountedRef.current = false }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const createSite = useCallback(async (data: SiteRequest) => {
const { data: created } = await sitesApi.create(data)
setSites((prev) => [...prev, created])
}, [])

const updateSite = useCallback(async (id: string, data: SiteRequest) => {
const { data: updated } = await sitesApi.update(id, data)
setSites((prev) => prev.map((s) => (s.id === id ? updated : s)))
}, [])

const deactivateSite = useCallback(async (id: string) => {
await sitesApi.deactivate(id)
setSites((prev) => prev.filter((s) => s.id !== id))
}, [])

const refresh = useCallback(async () => {
await fetchSites()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

return { sites, isLoading, error, createSite, updateSite, deactivateSite, refresh }
}
Loading
Loading