From f9cd469db9a16003fb5bca1c4e411ea7e5d0b389 Mon Sep 17 00:00:00 2001 From: Gitea Date: Wed, 24 Jun 2026 16:10:54 +0530 Subject: [PATCH] first commit --- .env | 1 + package-lock.json | 38 ++++ package.json | 4 +- public/CPM_LogoPrimary_Black.png | Bin 0 -> 10969 bytes public/index.html | 4 +- src/App.css | 43 ++-- src/App.js | 49 +++-- src/components/Dashboard.css | 97 +++++++++ src/components/Dashboard.js | 46 +++++ src/components/FileExplorer.css | 345 +++++++++++++++++++++++++++++++ src/components/FileExplorer.js | 344 ++++++++++++++++++++++++++++++ src/components/Login.css | 187 +++++++++++++++++ src/components/Login.js | 168 +++++++++++++++ src/utils/api.js | 37 ++++ 14 files changed, 1315 insertions(+), 48 deletions(-) create mode 100644 .env create mode 100644 public/CPM_LogoPrimary_Black.png create mode 100644 src/components/Dashboard.css create mode 100644 src/components/Dashboard.js create mode 100644 src/components/FileExplorer.css create mode 100644 src/components/FileExplorer.js create mode 100644 src/components/Login.css create mode 100644 src/components/Login.js create mode 100644 src/utils/api.js 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 0000000000000000000000000000000000000000..0954fa5bd1b2c2cf25c878cec779b2be3aff8708 GIT binary patch literal 10969 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%0A3!OB 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 - + - 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 (
); } 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}) + + +
+
+
+ +
+ +
+ +
+

CPM HR UTILITY 2026 | Role: {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;