Compare commits

..

No commits in common. "8d5b2d3ea31ba436f53bf5248f1a842a11ee0b13" and "cd06dbd9fc1a1533f7c25489b35cf3fb4e8ac9be" have entirely different histories.

11 changed files with 40 additions and 182 deletions

View File

@ -40,7 +40,4 @@ app.use("/admin", adminRoutes);
const usersRoutes = require("./routes/users.routes"); const usersRoutes = require("./routes/users.routes");
app.use("/users", usersRoutes); app.use("/users", usersRoutes);
const configRoutes = require("./routes/config.routes");
app.use("/config", configRoutes);
module.exports = app; module.exports = app;

View File

@ -1,15 +0,0 @@
/**
* Application-wide constants
* These are non-secret configuration values shared across the application
*/
module.exports = {
// File upload limits
MAX_FILE_SIZE_MB: 20,
MAX_FILE_SIZE_BYTES: 20 * 1024 * 1024,
// Image processing
MAX_IMAGE_DIMENSION: 800,
IMAGE_QUALITY: 85,
IMAGE_FORMAT: 'jpeg'
};

View File

@ -1,14 +0,0 @@
/**
* Configuration endpoints
* Public endpoints that provide application configuration to clients
*/
const { MAX_FILE_SIZE_MB, MAX_IMAGE_DIMENSION, IMAGE_QUALITY } = require("../config/constants");
exports.getConfig = (req, res) => {
res.json({
maxFileSizeMB: MAX_FILE_SIZE_MB,
maxImageDimension: MAX_IMAGE_DIMENSION,
imageQuality: IMAGE_QUALITY
});
};

View File

@ -1,12 +1,11 @@
const multer = require("multer"); const multer = require("multer");
const sharp = require("sharp"); const sharp = require("sharp");
const { MAX_FILE_SIZE_BYTES, MAX_IMAGE_DIMENSION, IMAGE_QUALITY } = require("../config/constants");
// Configure multer for memory storage (we'll process before saving to DB) // Configure multer for memory storage (we'll process before saving to DB)
const upload = multer({ const upload = multer({
storage: multer.memoryStorage(), storage: multer.memoryStorage(),
limits: { limits: {
fileSize: MAX_FILE_SIZE_BYTES, fileSize: 10 * 1024 * 1024, // 10MB max file size
}, },
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
// Only accept images // Only accept images
@ -25,13 +24,13 @@ const processImage = async (req, res, next) => {
} }
try { try {
// Compress and resize image using constants // Compress and resize image to 800x800px, JPEG quality 85
const processedBuffer = await sharp(req.file.buffer) const processedBuffer = await sharp(req.file.buffer)
.resize(MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION, { .resize(800, 800, {
fit: "inside", fit: "inside",
withoutEnlargement: true, withoutEnlargement: true,
}) })
.jpeg({ quality: IMAGE_QUALITY }) .jpeg({ quality: 85 })
.toBuffer(); .toBuffer();
// Attach processed image to request // Attach processed image to request

View File

@ -10,24 +10,18 @@ exports.getUnboughtItems = async () => {
gl.bought, gl.bought,
ENCODE(gl.item_image, 'base64') as item_image, ENCODE(gl.item_image, 'base64') as item_image,
gl.image_mime_type, gl.image_mime_type,
( ARRAY_AGG(DISTINCT COALESCE(gh_user.name, creator.name)) FILTER (WHERE COALESCE(gh_user.name, creator.name) IS NOT NULL) as added_by_users,
SELECT ARRAY_AGG(u.name ORDER BY gh.added_on DESC)
FROM (
SELECT gh.added_by, gh.added_on,
ROW_NUMBER() OVER (PARTITION BY gh.list_item_id ORDER BY gh.added_on DESC) as rn
FROM grocery_history gh
WHERE gh.list_item_id = gl.id
) gh
JOIN users u ON gh.added_by = u.id
WHERE gh.rn <= gl.quantity
) as added_by_users,
gl.modified_on as last_added_on, gl.modified_on as last_added_on,
ic.item_type, ic.item_type,
ic.item_group, ic.item_group,
ic.zone ic.zone
FROM grocery_list gl FROM grocery_list gl
LEFT JOIN users creator ON gl.added_by = creator.id
LEFT JOIN grocery_history gh ON gl.id = gh.list_item_id
LEFT JOIN users gh_user ON gh.added_by = gh_user.id
LEFT JOIN item_classification ic ON gl.id = ic.id LEFT JOIN item_classification ic ON gl.id = ic.id
WHERE gl.bought = FALSE WHERE gl.bought = FALSE
GROUP BY gl.id, gl.item_name, gl.quantity, gl.bought, gl.item_image, gl.image_mime_type, gl.modified_on, ic.item_type, ic.item_group, ic.zone
ORDER BY gl.id ASC` ORDER BY gl.id ASC`
); );
return result.rows; return result.rows;
@ -125,21 +119,15 @@ exports.getRecentlyBoughtItems = async () => {
gl.bought, gl.bought,
ENCODE(gl.item_image, 'base64') as item_image, ENCODE(gl.item_image, 'base64') as item_image,
gl.image_mime_type, gl.image_mime_type,
( ARRAY_AGG(DISTINCT COALESCE(gh_user.name, creator.name)) FILTER (WHERE COALESCE(gh_user.name, creator.name) IS NOT NULL) as added_by_users,
SELECT ARRAY_AGG(u.name ORDER BY gh.added_on DESC)
FROM (
SELECT gh.added_by, gh.added_on,
ROW_NUMBER() OVER (PARTITION BY gh.list_item_id ORDER BY gh.added_on DESC) as rn
FROM grocery_history gh
WHERE gh.list_item_id = gl.id
) gh
JOIN users u ON gh.added_by = u.id
WHERE gh.rn <= gl.quantity
) as added_by_users,
gl.modified_on as last_added_on gl.modified_on as last_added_on
FROM grocery_list gl FROM grocery_list gl
LEFT JOIN users creator ON gl.added_by = creator.id
LEFT JOIN grocery_history gh ON gl.id = gh.list_item_id
LEFT JOIN users gh_user ON gh.added_by = gh_user.id
WHERE gl.bought = TRUE WHERE gl.bought = TRUE
AND gl.modified_on >= NOW() - INTERVAL '24 hours' AND gl.modified_on >= NOW() - INTERVAL '24 hours'
GROUP BY gl.id, gl.item_name, gl.quantity, gl.bought, gl.item_image, gl.image_mime_type, gl.modified_on
ORDER BY gl.modified_on DESC` ORDER BY gl.modified_on DESC`
); );
return result.rows; return result.rows;

View File

@ -1,8 +0,0 @@
const express = require("express");
const router = express.Router();
const configController = require("../controllers/config.controller");
// Public endpoint - no authentication required
router.get("/", configController.getConfig);
module.exports = router;

View File

@ -1,7 +1,6 @@
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Route, Routes } from "react-router-dom";
import { ROLES } from "./constants/roles"; import { ROLES } from "./constants/roles";
import { AuthProvider } from "./context/AuthContext.jsx"; import { AuthProvider } from "./context/AuthContext.jsx";
import { ConfigProvider } from "./context/ConfigContext.jsx";
import AdminPanel from "./pages/AdminPanel.jsx"; import AdminPanel from "./pages/AdminPanel.jsx";
import GroceryList from "./pages/GroceryList.jsx"; import GroceryList from "./pages/GroceryList.jsx";
@ -16,39 +15,37 @@ import RoleGuard from "./utils/RoleGuard.jsx";
function App() { function App() {
return ( return (
<ConfigProvider> <AuthProvider>
<AuthProvider> <BrowserRouter>
<BrowserRouter> <Routes>
<Routes>
{/* Public route */} {/* Public route */}
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
{/* Private routes with layout */}
<Route
element={
<PrivateRoute>
<AppLayout />
</PrivateRoute>
}
>
<Route path="/" element={<GroceryList />} />
{/* Private routes with layout */}
<Route <Route
path="/admin"
element={ element={
<PrivateRoute> <RoleGuard allowed={[ROLES.ADMIN]}>
<AppLayout /> <AdminPanel />
</PrivateRoute> </RoleGuard>
} }
> />
<Route path="/" element={<GroceryList />} /> </Route>
<Route </Routes>
path="/admin" </BrowserRouter>
element={ </AuthProvider>
<RoleGuard allowed={[ROLES.ADMIN]}>
<AdminPanel />
</RoleGuard>
}
/>
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
</ConfigProvider>
); );
} }

View File

@ -1,10 +0,0 @@
import api from "./axios";
/**
* Fetch application configuration
* @returns {Promise<Object>} Configuration object with maxFileSizeMB, maxImageDimension, etc.
*/
export const getConfig = async () => {
const response = await api.get("/config");
return response.data;
};

View File

@ -1,5 +1,4 @@
import { useRef, useState, useContext } from "react"; import { useRef } from "react";
import { ConfigContext } from "../../context/ConfigContext";
import "../../styles/components/ImageUploadSection.css"; import "../../styles/components/ImageUploadSection.css";
/** /**
@ -18,25 +17,10 @@ export default function ImageUploadSection({
}) { }) {
const cameraInputRef = useRef(null); const cameraInputRef = useRef(null);
const galleryInputRef = useRef(null); const galleryInputRef = useRef(null);
const [sizeError, setSizeError] = useState(null);
const { config } = useContext(ConfigContext);
const MAX_FILE_SIZE = config ? config.maxFileSizeMB * 1024 * 1024 : 20 * 1024 * 1024;
const MAX_FILE_SIZE_MB = config ? config.maxFileSizeMB : 20;
const handleFileChange = (e) => { const handleFileChange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
// Check file size
if (file.size > MAX_FILE_SIZE) {
const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
setSizeError(`Image size (${sizeMB}MB) exceeds the ${MAX_FILE_SIZE_MB}MB limit. Please choose a smaller image.`);
// Reset the input
e.target.value = '';
return;
}
// Clear any previous error
setSizeError(null);
onImageChange(file); onImageChange(file);
} }
}; };
@ -52,11 +36,6 @@ export default function ImageUploadSection({
return ( return (
<div className="image-upload-section"> <div className="image-upload-section">
<h3 className="image-upload-title">{title}</h3> <h3 className="image-upload-title">{title}</h3>
{sizeError && (
<div className="image-upload-error">
{sizeError}
</div>
)}
<div className="image-upload-content"> <div className="image-upload-content">
{!imagePreview ? ( {!imagePreview ? (
<div className="image-upload-options"> <div className="image-upload-options">

View File

@ -1,44 +0,0 @@
import { createContext, useState, useEffect } from 'react';
import { getConfig } from '../api/config';
export const ConfigContext = createContext({
config: null,
loading: true,
});
export const ConfigProvider = ({ children }) => {
const [config, setConfig] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchConfig = async () => {
try {
const data = await getConfig();
setConfig(data);
} catch (error) {
console.error('Failed to fetch config:', error);
// Set default fallback values
setConfig({
maxFileSizeMB: 20,
maxImageDimension: 800,
imageQuality: 85
});
} finally {
setLoading(false);
}
};
fetchConfig();
}, []);
const value = {
config,
loading
};
return (
<ConfigContext.Provider value={value}>
{children}
</ConfigContext.Provider>
);
};

View File

@ -10,17 +10,6 @@
color: #333; color: #333;
} }
.image-upload-error {
background: #f8d7da;
border: 1px solid #f5c2c7;
color: #842029;
padding: 0.75rem;
border-radius: 6px;
margin-bottom: 0.8rem;
font-size: 0.9em;
line-height: 1.4;
}
.image-upload-content { .image-upload-content {
border: 2px dashed #ccc; border: 2px dashed #ccc;
border-radius: 8px; border-radius: 8px;