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
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"@tailwindcss/postcss": "^4.1.14",
"@tanstack/react-query": "^5.90.6",
"@tanstack/react-query-devtools": "^5.90.2",
"autoprefixer": "^10.4.21",
"axios": "^1.12.2",
"i18next": "^25.6.0",
Expand Down
135 changes: 69 additions & 66 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Tooltip } from "react-tooltip";
import { Toaster } from "react-hot-toast";
import { isAuthenticated } from "./api/auth";
import { SoundProvider } from "./hooks/useSoundEffects";
import { QueryProvider } from "./providers/QueryProvider";
import SoundToggle from "./components/SoundToggle";

const Login = lazy(() => import("./pages/Login"));
Expand All @@ -21,77 +22,79 @@ const LoadingSpinner = () => (

const App = () => {
return (
<SoundProvider>
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Suspense>
<Tooltip id="backup-tooltip" />
<Tooltip id="internal-url-tooltip" />
<Tooltip id="external-url-tooltip" />
<Toaster
position="top-right"
reverseOrder={false}
gutter={12}
toastOptions={{
duration: 4000,
style: {
borderRadius: "0.5rem",
padding: "1rem 1.25rem",
fontSize: "0.95rem",
fontWeight: "500",
boxShadow:
"0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.08)",
minWidth: "320px",
maxWidth: "420px",
},
success: {
<QueryProvider>
<SoundProvider>
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Suspense>
<Tooltip id="backup-tooltip" />
<Tooltip id="internal-url-tooltip" />
<Tooltip id="external-url-tooltip" />
<Toaster
position="top-right"
reverseOrder={false}
gutter={12}
toastOptions={{
duration: 4000,
style: {
background: "linear-gradient(135deg, #10b981 0%, #059669 100%)",
color: "#ffffff",
borderRadius: "0.5rem",
padding: "1rem 1.25rem",
fontSize: "0.95rem",
fontWeight: "500",
boxShadow:
"0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.08)",
minWidth: "320px",
maxWidth: "420px",
},
iconTheme: {
primary: "#ffffff",
secondary: "#10b981",
success: {
style: {
background: "linear-gradient(135deg, #10b981 0%, #059669 100%)",
color: "#ffffff",
},
iconTheme: {
primary: "#ffffff",
secondary: "#10b981",
},
},
},
error: {
style: {
background: "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)",
color: "#ffffff",
},
iconTheme: {
primary: "#ffffff",
secondary: "#ef4444",
},
},
loading: {
style: {
background: "linear-gradient(135deg, #a855f7 0%, #9333ea 100%)",
color: "#ffffff",
error: {
style: {
background: "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)",
color: "#ffffff",
},
iconTheme: {
primary: "#ffffff",
secondary: "#ef4444",
},
},
iconTheme: {
primary: "#ffffff",
secondary: "#a855f7",
loading: {
style: {
background: "linear-gradient(135deg, #a855f7 0%, #9333ea 100%)",
color: "#ffffff",
},
iconTheme: {
primary: "#ffffff",
secondary: "#a855f7",
},
},
},
}}
/>
<SoundToggle />
</BrowserRouter>
</SoundProvider>
}}
/>
<SoundToggle />
</BrowserRouter>
</SoundProvider>
</QueryProvider>
);
};

Expand Down
1 change: 0 additions & 1 deletion client/src/api/coolify.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import resourceRepository from "../repositories/ResourceRepository";

// Legacy exports for backward compatibility
export const fetchAllResources = () => resourceRepository.fetchAll();
export const getUserType = () => resourceRepository.getUserType();
export const startResource = (type, uuid) => resourceRepository.start(type, uuid);
export const stopResource = (type, uuid) => resourceRepository.stop(type, uuid);
export const deleteResource = (type, uuid) => resourceRepository.delete(type, uuid);
Expand Down
20 changes: 4 additions & 16 deletions client/src/components/ResourceCard/hooks/useResourceCardState.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { getUserType } from "../../../api/coolify";
import { useConfig } from "../../../hooks/useConfig";
import useResourceStore from "../../../store/resourceStore";

const useResourceCardState = (resource) => {
Expand All @@ -10,8 +10,9 @@ const useResourceCardState = (resource) => {
const [confirmUrl, setConfirmUrl] = useState(null);
const [confirmAction, setConfirmAction] = useState(null);

// User permissions
const [userType, setUserType] = useState(null);
// User permissions - fetch from global config cache
const { data: config } = useConfig();
const userType = config?.userType;

// Action timing
const [realtimeElapsed, setRealtimeElapsed] = useState(0);
Expand All @@ -21,19 +22,6 @@ const useResourceCardState = (resource) => {
const actionKey = `${resource.type}-${resource.uuid}`;
const currentAction = actionLoading[actionKey];

// Fetch user type on mount
useEffect(() => {
const fetchUserType = async () => {
try {
const type = await getUserType();
setUserType(type);
} catch (error) {
console.error("Failed to fetch user type:", error);
}
};
fetchUserType();
}, []);

// Handle action timing updates
useEffect(() => {
if (!currentAction) {
Expand Down
4 changes: 4 additions & 0 deletions client/src/components/SoundToggle.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { SpeakerWaveIcon, SpeakerXMarkIcon } from "@heroicons/react/24/outline";
import { useSoundEffects } from "../hooks/useSoundEffects";
import { SOUND_TYPES } from "../utils/soundUtils";
import { useIsMobile } from "../hooks/useIsMobile";

const SoundToggle = () => {
const { isSoundEnabled, toggleSound, playSound } = useSoundEffects();
const isMobile = useIsMobile();

if (isMobile) return null;

const handleClick = () => {
playSound(SOUND_TYPES.CLICK);
Expand Down
23 changes: 23 additions & 0 deletions client/src/hooks/useConfig.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useQuery } from "@tanstack/react-query";
import configRepository from "../repositories/ConfigRepository";

/**
* Hook to fetch and cache application configuration
*
* Configuration values are set at container startup and never change during runtime,
* so they are cached indefinitely (staleTime: Infinity)
*
* @returns {Object} Query result
* @returns {Object} data - Config data: { userType: 'admin' | 'member' | 'viewer' }
* @returns {boolean} isLoading - Loading state
* @returns {Error} error - Error if request failed
*/
export const useConfig = () => {
return useQuery({
queryKey: ["config"],
queryFn: () => configRepository.getConfig(),
staleTime: Infinity, // Config never goes stale (set at container startup)
gcTime: Infinity, // Keep in cache forever
retry: 3, // Retry failed requests (important for app initialization)
});
};
27 changes: 27 additions & 0 deletions client/src/hooks/useIsMobile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useState, useEffect } from "react";

export const useIsMobile = () => {
const [isMobile, setIsMobile] = useState(() => {
if (typeof window !== "undefined") {
return window.innerWidth < 768;
}
return false;
});

useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 767px)");

const handleChange = (e) => {
setIsMobile(e.matches);
};

setIsMobile(mediaQuery.matches);
mediaQuery.addEventListener("change", handleChange);

return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);

return isMobile;
};
6 changes: 5 additions & 1 deletion client/src/hooks/useSoundEffects.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
getSoundEnabled,
setSoundEnabled,
} from "../utils/soundUtils";
import { useIsMobile } from "./useIsMobile";

const SoundContext = createContext(null);

export const SoundProvider = ({ children }) => {
const isMobile = useIsMobile();
const [isSoundEnabled, setIsSoundEnabled] = useState(() => getSoundEnabled());

const toggleSound = useCallback(() => {
Expand All @@ -21,11 +23,13 @@ export const SoundProvider = ({ children }) => {

const playSound = useCallback(
(soundType, volume = 0.3) => {
if (isMobile) return;

if (isSoundEnabled) {
playSoundType(soundType, volume);
}
},
[isSoundEnabled]
[isSoundEnabled, isMobile]
);

const value = {
Expand Down
23 changes: 23 additions & 0 deletions client/src/providers/QueryProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

// Configure QueryClient with default options
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // Disable refetch on window focus
retry: 1, // Retry failed requests once
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
},
},
});

export const QueryProvider = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
{children}
{/* Show DevTools only in development */}
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
);
};
21 changes: 21 additions & 0 deletions client/src/repositories/ConfigRepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import apiClient from "../services/ApiClient";

/**
* Repository for application configuration
* Fetches static configuration from the server that doesn't change during runtime
*/
class ConfigRepository {
constructor(client = apiClient) {
this.client = client;
}

/**
* Fetch application configuration
* @returns {Promise<{userType: string}>} Configuration object
*/
async getConfig() {
return await this.client.get("/config", { baseURL: "/api" });
}
}

export default new ConfigRepository();
5 changes: 0 additions & 5 deletions client/src/repositories/ResourceRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,6 @@ class ResourceRepository {
async getLogs(type, uuid) {
return await this.client.get(`/${type}s/${uuid}/logs`);
}

async getUserType() {
const data = await this.client.get("/user-type");
return data.userType;
}
}

const resourceRepository = new ResourceRepository();
Expand Down
Loading