diff --git a/.env b/.env new file mode 100644 index 0000000..7fda242 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_API_URL=http://localhost:8981/api diff --git a/package-lock.json b/package-lock.json index 3aceaa9..a286dd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", + "axios": "^1.4.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-scripts": "5.0.1", @@ -4828,6 +4829,34 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -13553,6 +13582,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/package.json b/package.json index fdec449..1d7442c 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "client", "version": "0.1.0", "private": true, + "proxy": "http://localhost:8981", "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", @@ -10,7 +11,8 @@ "react": "^19.2.7", "react-dom": "^19.2.7", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "axios": "^1.4.0" }, "scripts": { "start": "react-scripts start", diff --git a/public/CPM_LogoPrimary_Black.png b/public/CPM_LogoPrimary_Black.png new file mode 100644 index 0000000..0954fa5 Binary files /dev/null and b/public/CPM_LogoPrimary_Black.png differ diff --git a/public/index.html b/public/index.html index aa069f2..ca23802 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - + - React App + CPM HR UTILITY diff --git a/src/App.css b/src/App.css index 74b5e05..2807621 100644 --- a/src/App.css +++ b/src/App.css @@ -1,38 +1,21 @@ .App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; + width: 100%; min-height: 100vh; +} + +.loading { display: flex; - flex-direction: column; - align-items: center; justify-content: center; - font-size: calc(10px + 2vmin); + align-items: center; + height: 100vh; + font-size: 24px; + color: #667eea; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +* { + margin: 0; + padding: 0; + box-sizing: border-box; } diff --git a/src/App.js b/src/App.js index 3784575..ff36fdb 100644 --- a/src/App.js +++ b/src/App.js @@ -1,23 +1,42 @@ -import logo from './logo.svg'; +import React, { useState, useEffect } from 'react'; import './App.css'; +import Login from './components/Login'; +import Dashboard from './components/Dashboard'; function App() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if user is already logged in + const savedUser = localStorage.getItem('user'); + if (savedUser) { + setUser(JSON.parse(savedUser)); + } + setLoading(false); + }, []); + + const handleLogin = (userData) => { + setUser(userData); + }; + + const handleLogout = () => { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + setUser(null); + }; + + if (loading) { + return
Loading...
; + } + return (
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
+ {user ? ( + + ) : ( + + )}
); } diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css new file mode 100644 index 0000000..a65fc44 --- /dev/null +++ b/src/components/Dashboard.css @@ -0,0 +1,97 @@ +.dashboard { + min-height: 100vh; + display: flex; + flex-direction: column; + background: #f5f5f5; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.dashboard-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.header-content { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.dashboard-header h1 { + margin: 0; + font-size: 28px; +} + +.user-info { + display: flex; + align-items: center; + gap: 20px; +} + +.user-badge { + background: rgba(255, 255, 255, 0.2); + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; +} + +.logout-btn { + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; +} + +.logout-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); +} + +.dashboard-main { + flex: 1; + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 30px 20px; +} + +.dashboard-footer { + background: #333; + color: white; + text-align: center; + padding: 20px; + font-size: 14px; +} + +.dashboard-footer p { + margin: 0; +} + +@media (max-width: 768px) { + .header-content { + flex-direction: column; + gap: 15px; + } + + .dashboard-header h1 { + font-size: 22px; + } + + .user-info { + width: 100%; + justify-content: space-between; + } + + .dashboard-main { + padding: 15px 10px; + } +} diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js new file mode 100644 index 0000000..e638612 --- /dev/null +++ b/src/components/Dashboard.js @@ -0,0 +1,46 @@ +import React from 'react'; +import FileExplorer from './FileExplorer'; +import './Dashboard.css'; + +function Dashboard({ user, onLogout }) { + const getRoleIcon = (role) => { + return role === 'HR' ? '👔' : '📋'; + }; + + return ( +
+
+
+
+ CPM Logo + {/* Compressor */} +
+ +
+ + + {getRoleIcon(user.role)} {user.username} ({user.role}) + + +
+
+
+ +
+ +
+ + +
+ ); +} + +export default Dashboard; diff --git a/src/components/FileExplorer.css b/src/components/FileExplorer.css new file mode 100644 index 0000000..08807bb --- /dev/null +++ b/src/components/FileExplorer.css @@ -0,0 +1,345 @@ +.file-explorer { + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.file-explorer h2 { + color: #333; + margin: 0 0 30px 0; + font-size: 24px; + border-bottom: 2px solid #667eea; + padding-bottom: 15px; +} + +.section { + margin-bottom: 40px; +} + +.section h3 { + color: #333; + font-size: 18px; + margin: 0 0 20px 0; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +/* Upload Section */ +.upload-section { + margin-bottom: 40px; + padding: 20px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fafbff; +} + +.upload-actions { + display: flex; + gap: 16px; + align-items: center; + flex-wrap: wrap; + margin-top: 16px; +} + +.upload-button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 24px; + border-radius: 10px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.upload-button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 12px 24px rgba(102, 126, 234, 0.22); +} + +.upload-button input[type="file"] { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; +} + +.upload-button:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.upload-help { + color: #4a5568; + font-size: 13px; +} + +/* Controls */ +.controls { + display: flex; + gap: 20px; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.quality-control { + display: flex; + align-items: center; + gap: 10px; +} + +.quality-control label { + color: #333; + font-size: 14px; + white-space: nowrap; +} + +.quality-control input[type="range"] { + width: 150px; +} + +/* Buttons */ +.select-all-btn, +.compress-btn, +.download-all-btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.select-all-btn { + background: #f0f0f0; + color: #333; +} + +.select-all-btn:hover:not(:disabled) { + background: #e0e0e0; +} + +.compress-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.compress-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); +} + +.compress-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.download-all-btn { + background: #4CAF50; + color: white; +} + +.download-all-btn:hover:not(:disabled) { + background: #45a049; + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3); +} + +.download-all-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Files Grid */ +.files-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.file-card { + background: #f9f9f9; + border: 2px solid #eee; + border-radius: 8px; + padding: 15px; + text-align: center; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.file-card:hover { + border-color: #667eea; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +.file-card.selected { + background: #e8f5e9; + border-color: #4CAF50; +} + +.file-card.compressing { + opacity: 0.6; + pointer-events: none; +} + +.file-card.downloading { + opacity: 0.6; +} + +.file-checkbox { + position: absolute; + top: 10px; + left: 10px; + width: 20px; + height: 20px; + cursor: pointer; +} + +.file-thumbnail { + width: 100%; + height: 120px; + object-fit: cover; + border-radius: 6px; + margin: 10px 0; + display: block; +} + +.file-info { + margin-top: 10px; +} + +.file-name { + color: #333; + font-size: 12px; + margin: 5px 0; + word-break: break-word; + font-weight: 500; +} + +.file-size { + color: #999; + font-size: 11px; + margin: 5px 0; +} + +.badge { + display: inline-block; + background: #667eea; + color: white; + padding: 3px 8px; + border-radius: 12px; + font-size: 10px; + margin-top: 5px; +} + +/* Compressed Files Section */ +.output-section { + margin-top: 40px; + border-top: 2px solid #eee; + padding-top: 30px; +} + +.file-card.compressed { + padding: 10px; +} + +.file-actions { + display: flex; + gap: 8px; + justify-content: center; + margin-bottom: 10px; +} + +.action-btn { + width: 36px; + height: 36px; + border: none; + border-radius: 6px; + font-size: 18px; + cursor: pointer; + transition: all 0.3s ease; + background: #f0f0f0; +} + +.action-btn:hover:not(:disabled) { + background: #e0e0e0; + transform: scale(1.1); +} + +.action-btn.download { + background: #4CAF50; + color: white; +} + +.action-btn.download:hover:not(:disabled) { + background: #45a049; +} + +.action-btn.delete { + background: #f44336; + color: white; +} + +.action-btn.delete:hover:not(:disabled) { + background: #da190b; +} + +.action-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Responsive */ +@media (max-width: 768px) { + .file-explorer { + padding: 20px; + } + + .file-explorer h2 { + font-size: 18px; + } + + .controls { + flex-direction: column; + align-items: stretch; + } + + .quality-control { + flex-direction: column; + } + + .compress-btn, + .download-all-btn, + .select-all-btn { + width: 100%; + } + + .files-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 15px; + } + + .file-thumbnail { + height: 100px; + } +} + + diff --git a/src/components/FileExplorer.js b/src/components/FileExplorer.js new file mode 100644 index 0000000..0f29ebe --- /dev/null +++ b/src/components/FileExplorer.js @@ -0,0 +1,344 @@ +import React, { useState, useEffect } from 'react'; +import { filesAPI } from '../utils/api'; +import './FileExplorer.css'; + +function FileExplorer({ userRole }) { + const [files, setFiles] = useState([]); + const [outputFiles, setOutputFiles] = useState([]); + const [selectedFiles, setSelectedFiles] = useState(new Set()); + const [compression, setCompression] = useState(false); + const [compressQuality, setCompressQuality] = useState(80); + const [targetKb, setTargetKb] = useState(''); + const [loading, setLoading] = useState(false); + const [compressingFiles, setCompressingFiles] = useState(new Set()); + const [downloadingFiles, setDownloadingFiles] = useState(new Set()); + const [uploading, setUploading] = useState(false); + + useEffect(() => { + loadFiles(); + loadOutputFiles(); + }, []); + + const loadFiles = async () => { + try { + const response = await filesAPI.list(); + setFiles(response.data.files || []); + } catch (err) { + console.error('Error loading files:', err); + } + }; + + const loadOutputFiles = async () => { + try { + const response = await filesAPI.getOutputs(); + setOutputFiles(response.data.files || []); + } catch (err) { + console.error('Error loading output files:', err); + } + }; + + const refreshSourceFiles = async () => { + setLoading(true); + try { + await loadFiles(); + } finally { + setLoading(false); + } + }; + + const uploadFiles = async (files) => { + const fileArray = Array.from(files || []); + if (fileArray.length === 0) { + return; + } + + setUploading(true); + try { + const formData = new FormData(); + if (fileArray.length === 1) { + formData.append('image', fileArray[0]); + await filesAPI.upload(formData); + } else { + fileArray.forEach((file) => formData.append('images', file)); + await filesAPI.uploadMultiple(formData); + } + await loadFiles(); + } catch (err) { + const message = err.response?.data?.error || err.message; + alert('Upload failed: ' + message); + } finally { + setUploading(false); + } + }; + + const toggleSelectFile = (filename) => { + const newSelected = new Set(selectedFiles); + if (newSelected.has(filename)) { + newSelected.delete(filename); + } else { + newSelected.add(filename); + } + setSelectedFiles(newSelected); + }; + + const selectAllFiles = () => { + if (selectedFiles.size === files.length) { + setSelectedFiles(new Set()); + } else { + setSelectedFiles(new Set(files.map(f => f.name))); + } + }; + + const compressSelected = async () => { + if (selectedFiles.size === 0) { + alert('Please select files to compress'); + return; + } + + setCompression(true); + setCompressingFiles(new Set(selectedFiles)); + + try { + const response = await filesAPI.compressMultiple( + Array.from(selectedFiles), + compressQuality, + targetKb + ); + + alert(`${response.data.files.length} files processed successfully!`); + setSelectedFiles(new Set()); + setTargetKb(''); + + await loadOutputFiles(); + } catch (err) { + const message = err.response?.data?.error || err.message; + alert('Compression failed: ' + message); + } finally { + setCompression(false); + setCompressingFiles(new Set()); + } + }; + + const downloadFile = async (filename) => { + setDownloadingFiles(prev => new Set([...prev, filename])); + try { + const response = await filesAPI.downloadFile(filename); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + link.parentNode.removeChild(link); + await loadOutputFiles(); + await loadFiles(); + } catch (err) { + const message = err.response?.data?.error || err.message; + alert('Download failed: ' + message); + } finally { + setDownloadingFiles(prev => { + const newSet = new Set(prev); + newSet.delete(filename); + return newSet; + }); + } + }; + + const downloadAllSelected = async () => { + if (outputFiles.length === 0) { + alert('No files to download'); + return; + } + + setDownloadingFiles(new Set(outputFiles.map(f => f.name))); + try { + const response = await filesAPI.downloadZip(outputFiles.map(f => f.name)); + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `compressed-files-${Date.now()}.zip`); + document.body.appendChild(link); + link.click(); + link.parentNode.removeChild(link); + + await loadOutputFiles(); + await loadFiles(); + setSelectedFiles(new Set()); + } catch (err) { + alert('Download failed: ' + err.message); + } finally { + setDownloadingFiles(new Set()); + } + }; + const deleteFile = async (filename) => { + if (window.confirm('Delete this file?')) { + try { + await filesAPI.deleteFile(filename); + loadOutputFiles(); + } catch (err) { + const message = err.response?.data?.error || err.message; + alert('Delete failed: ' + message); + } + } + }; + + return ( +
+

+ +
+
+
+

📂 Source Folder Upload

+

+ Upload single or multiple JPG Images or Pdf. The source folder is cleared before the new images or pdf are saved. +

+
+ +
+ +
+ + + Choose up to 100 images or Pdf to replace the current source folder. + +
+
+ + +
+
+

📸 Source Files (Images & PDFs) ({files.length})

+
+ + {files.length === 0 ? ( +

No source files yet. Upload images or PDFs to see them here.

+ ) : ( + <> +
+
+ + setCompressQuality(e.target.value)} + /> +
+
+ + setTargetKb(e.target.value)} + placeholder="e.g. 200" + min="20" + /> +
+ + +
+ +
+ {files.map(file => ( +
+ toggleSelectFile(file.name)} + className="file-checkbox" + /> + {file.extension === '.pdf' ? ( +
PDF
+ ) : ( + {file.name} + )} +
+

{file.name}

+

{(file.size / 1024).toFixed(2)} KB

+
+
+ ))} +
+ + )} +
+ + {/* Compressed Files Section */} +
+
+

✅ Compressed Files (Images & PDFs) ({outputFiles.length})

+ +
+ + {outputFiles.length === 0 ? ( +

No compressed Files yet. Compress source files to see them here.

+ ) : ( +
+ {outputFiles.map(file => ( +
+
+ + +
+ {file.name.toLowerCase().endsWith('.pdf') ? ( +
PDF
+ ) : ( + {file.name} + )} +
+

{file.name}

+

{(file.size / 1024).toFixed(2)} KB

+
+
+ ))} +
+ )} +
+
+ ); +} + +export default FileExplorer; diff --git a/src/components/Login.css b/src/components/Login.css new file mode 100644 index 0000000..cf49c4d --- /dev/null +++ b/src/components/Login.css @@ -0,0 +1,187 @@ +.login-container { + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.login-card { + background: white; + border-radius: 12px; + padding: 40px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + width: 100%; + max-width: 400px; + animation: slideUp 0.5s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.login-card h1 { + text-align: center; + color: #333; + margin-bottom: 30px; + font-size: 28px; +} + +.role-selector { + margin-bottom: 30px; +} + +.role-selector h3 { + color: #666; + font-size: 14px; + margin-bottom: 10px; + text-transform: uppercase; +} + +.role-selector { + display: flex; + flex-direction: column; + gap: 8px; +} + +.role-btn { + background: #f0f0f0; + border: 2px solid #ddd; + border-radius: 8px; + padding: 12px; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; +} + +.role-btn:hover { + border-color: #667eea; + background: #f5f5ff; +} + +.role-btn.active { + background: #667eea; + color: white; + border-color: #667eea; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + color: #333; + font-weight: 500; + font-size: 14px; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.3s ease; + box-sizing: border-box; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.error-message { + background: #fee; + color: #c33; + padding: 12px; + border-radius: 6px; + margin-bottom: 20px; + font-size: 14px; +} + +.submit-btn { + width: 100%; + padding: 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 6px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease; +} + +.submit-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 5px 20px rgba(102, 126, 234, 0.3); +} + +.submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.toggle-auth { + text-align: center; + margin-top: 20px; +} + +.toggle-auth p { + color: #666; + font-size: 14px; + margin: 0; +} + +.toggle-btn { + background: none; + border: none; + color: #667eea; + cursor: pointer; + font-weight: 600; + text-decoration: underline; + margin-left: 5px; +} + +.toggle-btn:hover { + color: #764ba2; +} + + +.login-logo-section { + text-align: center; + margin-bottom: 25px; +} + +.cpm-logo { + width: 220px; + height: auto; + object-fit: contain; +} + +.login-title { + margin: 0; + font-size: 28px; + font-weight: 700; + color: #003366; +} + +.login-subtitle { + margin-top: 8px; + color: #666; + font-size: 14px; +} + diff --git a/src/components/Login.js b/src/components/Login.js new file mode 100644 index 0000000..038bb65 --- /dev/null +++ b/src/components/Login.js @@ -0,0 +1,168 @@ +import React, { useState } from 'react'; +import { authAPI } from '../utils/api'; +import './Login.css'; + +function Login({ onLogin }) { + const [isLogin, setIsLogin] = useState(true); + const [email, setEmail] = useState('hr@company.com'); + const [password, setPassword] = useState('password123'); + const [username, setUsername] = useState(''); + const [role, setRole] = useState('HR'); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + if (isLogin) { + const response = await authAPI.login({ email, password }); + + localStorage.setItem('token', response.data.token); + localStorage.setItem('user', JSON.stringify(response.data.user)); + + onLogin(response.data.user); + } else { + const response = await authAPI.signup({ + username, + email, + password, + role, + }); + + localStorage.setItem('token', response.data.token); + localStorage.setItem('user', JSON.stringify(response.data.user)); + + onLogin(response.data.user); + } + } catch (err) { + setError(err.response?.data?.error || 'Something went wrong'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + {/* CPM LOGO */} +
+ CPM Logo +

+ CPM HR Utility Portal +

+
+ +
+

Accounts

+ + + + +
+ +
+ {!isLogin && ( + <> +
+ + + setUsername(e.target.value)} + required + /> +
+ +
+ + + +
+ + )} + +
+ + + setEmail(e.target.value)} + required + /> +
+ +
+ + + setPassword(e.target.value)} + required + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+ ); +} + +export default Login; \ No newline at end of file diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..a94d293 --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,37 @@ +import axios from 'axios'; + +const API_URL = process.env.REACT_APP_API_URL; + +const api = axios.create({ + baseURL: API_URL +}); + +// Add token to requests +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Auth endpoints +export const authAPI = { + signup: (data) => api.post('/auth/signup', data), + login: (data) => api.post('/auth/login', data) +}; + +// Files endpoints +export const filesAPI = { + list: () => api.get('/files/list'), + upload: (formData) => api.post('/files/upload', formData), + uploadMultiple: (formData) => api.post('/files/upload-multiple', formData), + compress: (filename, quality, targetKb) => api.post(`/files/compress/${filename}`, { quality, targetKb }), + compressMultiple: (filenames, quality, targetKb) => api.post('/files/compress-multiple', { filenames, quality, targetKb }), + downloadFile: (filename) => api.get(`/files/download/${filename}`, { responseType: 'blob' }), + downloadZip: (filenames) => api.post('/files/download-zip', { filenames }, { responseType: 'blob' }), + getOutputs: () => api.get('/files/outputs'), + deleteFile: (filename) => api.delete(`/files/delete/${filename}`) +}; + +export default api;