final version
complete redesign
9
frontend/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
# imk
|
||||||
16
frontend/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<!-- <link rel="icon" type="image/svg+xml" href="/logo.svg" /> -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>IMK</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
4536
frontend/package-lock.json
generated
Normal file
37
frontend/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "imk",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^1.7.19",
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"axios": "^1.7.7",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"framer-motion": "^11.18.2",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-router-dom": "^6.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
frontend/public/1.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
frontend/public/10.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/public/2.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
frontend/public/3.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/4.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
frontend/public/5.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
frontend/public/6.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/public/7.jpg
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
frontend/public/8.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
frontend/public/9.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/public/armatura.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/public/background.jpg
Normal file
|
After Width: | Height: | Size: 1003 KiB |
BIN
frontend/public/consalting.webp
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
frontend/public/consalting1.jpg
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
frontend/public/consulting2.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/public/ekstrakcija.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
frontend/public/geotehnika.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/public/imklogorgb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/public/kinenje.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
frontend/public/kocka.webp
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
frontend/public/kocki.webp
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
frontend/public/lab.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
frontend/public/lab.webp
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
frontend/public/labFinal.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/public/labfinal.webp
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
frontend/public/logo.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
frontend/public/naum1.webp
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
frontend/public/naum2.webp
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
frontend/public/profili.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
frontend/public/sertifikat1.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
frontend/public/sertifikat2.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
frontend/public/sertifikat3.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
frontend/public/sertifikat4.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
frontend/public/tuf.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/public/wallscener.jpeg
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
0
frontend/src/App.css
Normal file
60
frontend/src/App.jsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// import './App.css'
|
||||||
|
import { AuthProvider } from './hooks/useAuth';
|
||||||
|
import Navbar from './components/navbar/Navbar'
|
||||||
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import Home from './pages/homepage/Home'
|
||||||
|
import About from './pages/aboutpage/About'
|
||||||
|
import Contact from './pages/contactpage/Contact.jsx'
|
||||||
|
import Consulting from './components/consalting/consulting';
|
||||||
|
import Lab from './components/lab/lab';
|
||||||
|
import Footer from './components/footer/footer';
|
||||||
|
import UltraSound from './components/ultrasound/ultraSound';
|
||||||
|
import Gallery from './components/gallery/Gallery.jsx';
|
||||||
|
import Certificates from './components/Certificates/Certificates.jsx';
|
||||||
|
import Clients from './components/clients/clients';
|
||||||
|
import AdminPanel from './components/adminPanel/AdminPanel';
|
||||||
|
import Dashboard from './components/dashboard/Dashboard';
|
||||||
|
import Login from './components/login/login';
|
||||||
|
import ProtectedRoute from './components/protectedRoute/ProtectedRoute';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<div >
|
||||||
|
<BrowserRouter >
|
||||||
|
<Navbar />
|
||||||
|
<Routes>
|
||||||
|
<Route path='/' element={<Home />} />
|
||||||
|
<Route path='/contact' element={<Contact />} />
|
||||||
|
<Route path='/about' element={<About />} />
|
||||||
|
<Route path='/consulting' element={<Consulting />} />
|
||||||
|
<Route path='/lab' element={<Lab />} />
|
||||||
|
<Route path='/ultrasound' element={<UltraSound />} />
|
||||||
|
<Route path='/gallery' element={<Gallery />} />
|
||||||
|
<Route path='/certificates' element={<Certificates />} />
|
||||||
|
<Route path='/clients' element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Clients />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path='/admin' element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AdminPanel />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path='/login' element={<Login />} />
|
||||||
|
<Route path='/dashboard' element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
|
<Footer />
|
||||||
|
</BrowserRouter>
|
||||||
|
</div>
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
BIN
frontend/src/assets/consalting1.jpg
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
frontend/src/assets/consulting2.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
frontend/src/assets/imklogorgb.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/src/assets/lab.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
frontend/src/assets/logo.jpg
Normal file
|
After Width: | Height: | Size: 159 KiB |
32
frontend/src/components/Certificates/Certificates.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
function Certificates() {
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="cert-container">
|
||||||
|
<div className="img-container1">
|
||||||
|
<img src="/sertifikat1.jpg" alt="Image 1" />
|
||||||
|
</div>
|
||||||
|
<div className="img-container1">
|
||||||
|
<img src="sertifikat2.jpg" alt="Image 2" />
|
||||||
|
</div>
|
||||||
|
{/* <div className="img-container1">
|
||||||
|
<img src="sertifikat3.jpg" alt="Image 3" />
|
||||||
|
</div>
|
||||||
|
<div className="img-container1">
|
||||||
|
<img src="sertifikat4" alt="Image 4" />
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Certificates
|
||||||
84
frontend/src/components/UserCreate.jsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { createUser } from '../services/api';
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
function UserCreate({ onUserCreated }) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createUser(formData);
|
||||||
|
setFormData({ name: '', email: '', password: '' });
|
||||||
|
if (onUserCreated) onUserCreated();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.message || 'Failed to create user');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Create New User</h3>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-500 text-sm">{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white
|
||||||
|
${loading ? 'bg-gray-400' : 'bg-indigo-600 hover:bg-indigo-700'}`}
|
||||||
|
>
|
||||||
|
{loading ? 'Creating...' : 'Create User'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserCreate;
|
||||||
274
frontend/src/components/adminPanel/AdminPanel.jsx
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { getAllUsers, getAllDocuments, getUserInfo, createUser } from '../../services/api';
|
||||||
|
import DocumentUpload from '../documentUpload/DocumentUpload';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { FiUsers, FiFile, FiUpload, FiUserPlus, FiLoader } from 'react-icons/fi';
|
||||||
|
|
||||||
|
function AdminPanel() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [activeTab, setActiveTab] = useState('documents');
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [documents, setDocuments] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
const [newUser, setNewUser] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
isAdmin: false
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAdminStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAdmin) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [activeTab, isAdmin]);
|
||||||
|
|
||||||
|
const checkAdminStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getUserInfo();
|
||||||
|
if (!response?.data?.isAdmin) {
|
||||||
|
navigate('/');
|
||||||
|
} else {
|
||||||
|
setIsAdmin(true);
|
||||||
|
await fetchData();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Admin check failed:', error);
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
if (activeTab === 'users') {
|
||||||
|
const response = await getAllUsers();
|
||||||
|
setUsers(response.data);
|
||||||
|
} else if (activeTab === 'documents') {
|
||||||
|
const response = await getAllDocuments();
|
||||||
|
console.log('Documents data:', response.data);
|
||||||
|
setDocuments(response.data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fetch error:', err);
|
||||||
|
setError('Failed to fetch data. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateUser = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
await createUser(newUser);
|
||||||
|
setNewUser({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
isAdmin: false
|
||||||
|
});
|
||||||
|
fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to create user');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isAdmin) return null;
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'documents', name: 'Documents', icon: FiFile },
|
||||||
|
{ id: 'users', name: 'Users', icon: FiUsers },
|
||||||
|
{ id: 'upload', name: 'Upload Document', icon: FiUpload }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
<div className="flex items-center space-x-3 text-blue-500">
|
||||||
|
<FiLoader className="w-6 h-6 animate-spin" />
|
||||||
|
<span className="text-lg font-medium">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<header className="mb-8 mt-20">
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">Admin Dashboard</h1>
|
||||||
|
<p className="text-gray-400">Manage users and documents</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex space-x-4 mb-6">
|
||||||
|
{tabs.map(({ id, name, icon: Icon }) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
onClick={() => setActiveTab(id)}
|
||||||
|
className={`
|
||||||
|
px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors
|
||||||
|
${activeTab === id
|
||||||
|
? 'bg-white/10 text-white'
|
||||||
|
: 'text-gray-400 hover:bg-white/5 hover:text-white'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
<span>{name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{activeTab === 'documents' && (
|
||||||
|
<div className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-700">
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Title</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Status</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Uploaded By</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Shared With</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Created At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{documents.map((doc) => (
|
||||||
|
<tr key={doc.id} className="border-b border-gray-700/50 hover:bg-white/5">
|
||||||
|
<td className="px-6 py-4 text-gray-200">{doc.title}</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||||
|
doc.status === 'completed' ? 'bg-green-500/10 text-green-400' :
|
||||||
|
doc.status === 'pending' ? 'bg-yellow-500/10 text-yellow-400' :
|
||||||
|
doc.status === 'uploading' ? 'bg-blue-500/10 text-blue-400' :
|
||||||
|
'bg-gray-500/10 text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{doc.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-400">
|
||||||
|
{doc.uploadedBy?.name} ({doc.uploadedBy?.email})
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-400">
|
||||||
|
{doc.sharedWith && doc.sharedWith.length > 0
|
||||||
|
? doc.sharedWith.map(user => (
|
||||||
|
<div key={user.id} className="whitespace-nowrap">
|
||||||
|
{user.name} ({user.email})
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: 'None'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-400">
|
||||||
|
{new Date(doc.createdAt).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'users' && (
|
||||||
|
<>
|
||||||
|
<div className="bg-white/5 backdrop-blur-lg rounded-xl p-6 mb-6">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-4">Create New User</h2>
|
||||||
|
<form onSubmit={handleCreateUser} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
value={newUser.name}
|
||||||
|
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
|
||||||
|
className="bg-white/5 border border-gray-700 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={newUser.email}
|
||||||
|
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
|
||||||
|
className="bg-white/5 border border-gray-700 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={newUser.password}
|
||||||
|
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
|
||||||
|
className="bg-white/5 border border-gray-700 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="isAdmin"
|
||||||
|
checked={newUser.isAdmin}
|
||||||
|
onChange={(e) => setNewUser({ ...newUser, isAdmin: e.target.checked })}
|
||||||
|
className="rounded border-gray-700 bg-white/5 text-blue-500 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="isAdmin" className="text-gray-200">Is Admin</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex items-center justify-center space-x-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FiUserPlus className="w-4 h-4" />
|
||||||
|
<span>Create User</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-700">
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Name</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Email</th>
|
||||||
|
<th className="px-6 py-4 text-left text-sm text-gray-400">Role</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id} className="border-b border-gray-700/50 hover:bg-white/5">
|
||||||
|
<td className="px-6 py-4 text-gray-200">{user.name}</td>
|
||||||
|
<td className="px-6 py-4 text-gray-200">{user.email}</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium
|
||||||
|
${user.isAdmin ? 'bg-purple-500/10 text-purple-400' : 'bg-gray-500/10 text-gray-400'}`}>
|
||||||
|
{user.isAdmin ? 'Admin' : 'User'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'upload' && (
|
||||||
|
<div className="bg-white/5 backdrop-blur-lg rounded-xl p-6">
|
||||||
|
<DocumentUpload />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminPanel;
|
||||||
93
frontend/src/components/clients/clients.jsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
import { getSharedDocuments } from '../../services/api';
|
||||||
|
|
||||||
|
function Clients() {
|
||||||
|
const [documents, setDocuments] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDocuments = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getSharedDocuments(user.id);
|
||||||
|
setDocuments(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to fetch documents');
|
||||||
|
console.error('Error fetching documents:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
fetchDocuments();
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-center text-red-600 p-4">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Your Documents</h1>
|
||||||
|
|
||||||
|
{documents.length === 0 ? (
|
||||||
|
<p className="text-gray-600">No documents have been shared with you yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{documents.map((doc) => (
|
||||||
|
<div
|
||||||
|
key={doc.id}
|
||||||
|
className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<h2 className="text-xl font-semibold">{doc.title}</h2>
|
||||||
|
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||||
|
doc.status === 'completed'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-yellow-100 text-yellow-800'
|
||||||
|
}`}>
|
||||||
|
{doc.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
<p>Created: {new Date(doc.createdAt).toLocaleDateString()}</p>
|
||||||
|
{doc.content && (
|
||||||
|
<p className="mt-2">{doc.content}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => window.open(`/api/documents/download/${doc.s3Key}`, '_blank')}
|
||||||
|
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Clients;
|
||||||
|
|
||||||
27
frontend/src/components/consalting/consulting.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function Consulting() {
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
}, [])
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="constaltingContainer">
|
||||||
|
<h1 className="text-4xl text-center mt-32 font-bold tracking-tight text-gray-900 sm:text-3xl">Преглед на консултантски услуги</h1>
|
||||||
|
<div className="anotherContainer text-center">
|
||||||
|
<p className="tracking-tight text-gray-900 text-center sm:text-3xl mt-16">
|
||||||
|
ИМК нуди низа консултантски услуги, предлагајќи комплексен пристап за поддршка на различни аспекти на индустриските операции. Услугите вклучуваат изработка на интегрирани дозволи, проценки на влијанието врз животната средина, проектна документација и студии за финансиска исплатливост на индустриските процеси.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<ul className="tracking-tight text-gray-900 sm:text-2xl mt-16 text-center md:text-justify">
|
||||||
|
<li><strong>Интегрирани Дозволи (А и Б):</strong> Компанијата нуди услуги поврзани со изработка на интегрирани дозволи. Ова покажува експертиза во справувањето со регулаторните рамки и обезбедување на согласност со лиценцните барања. Интегрираните дозволи се суштински за индустриските операции, што укажува на фокус на оптимизација на правните и регулаторни процеси.</li>
|
||||||
|
<li><strong>Проценка на Влијанието врз Животната Средина:</strong> Консултантот обезбедува услуги за проценка на влијанието врз животната средина. Ова вклучува систематска евалуација на тоа како индустриските активности можат да влијаат на околната средина. Таквите проценки се суштински за обезбедување на одржливи практики и согласност со еколошките регулативи.</li>
|
||||||
|
<li><strong>Проектна Документација за Производствени Објекти:</strong> ИМК нуди изработка на проектна документација специфично дизајнирана за производствени објекти, земајки ги предвид различните аспекти на дизајнирање и оптимизација на индустриските операции.</li>
|
||||||
|
<li><strong>Проекти за Финасиска Ефикасност за Производствени Објекти:</strong> ИМК нуди експертиза во креирање на ценовна ефикасност за производствени капацитети. Ова вклучува анализа на постоечките процеси и идентификација на можностите за оптимизација за подобрување на продуктивноста и намалување на оперативните трошоци. Фокусот на ценовната ефикасност покажува посветеност кон подобрување на општата финансиска перформанса на индустриските операции.</li>
|
||||||
|
<li><strong>Елаборати за Финансиска Исплатливост на Производствени Процеси:</strong> ИМК изработува елаборати (детални извештаи) фокусирани на проценка на финансиската исплатливост на производствените процеси. Тие ги вклучуваат економските можности и оправданоста/одржливоста на проектираните или постоечките производствени процеси.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
154
frontend/src/components/dashboard/Dashboard.jsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
import { getSharedDocuments } from '../../services/api';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader } from 'react-icons/fi';
|
||||||
|
import { downloadDocument } from '../../services/api';
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const [documents, setDocuments] = useState({});
|
||||||
|
const [expandedFolders, setExpandedFolders] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDocuments = async () => {
|
||||||
|
try {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
const response = await getSharedDocuments(user.id);
|
||||||
|
// Group the documents by company and date
|
||||||
|
const groupedDocs = groupDocumentsByCompanyAndDate(response.data);
|
||||||
|
setDocuments(groupedDocs);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching documents:', err);
|
||||||
|
setError('Failed to fetch documents');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDocuments();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const groupDocumentsByCompanyAndDate = (docs) => {
|
||||||
|
if (!Array.isArray(docs)) {
|
||||||
|
console.error('Expected array of documents, received:', docs);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return docs.reduce((acc, doc) => {
|
||||||
|
const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
|
||||||
|
if (!acc[folderName]) {
|
||||||
|
acc[folderName] = [];
|
||||||
|
}
|
||||||
|
acc[folderName].push(doc);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = async (s3Key, fileName) => {
|
||||||
|
try {
|
||||||
|
await downloadDocument(s3Key);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error downloading document:', err);
|
||||||
|
alert('Failed to download document. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFolder = (folderName) => {
|
||||||
|
setExpandedFolders(prev => ({
|
||||||
|
...prev,
|
||||||
|
[folderName]: !prev[folderName]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
<div className="flex items-center space-x-3 text-blue-500">
|
||||||
|
<FiLoader className="w-6 h-6 animate-spin" />
|
||||||
|
<span className="text-lg font-medium">Loading documents...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<header className="mb-8 mt-20">
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">Your Documents</h1>
|
||||||
|
<p className="text-gray-400">Access and manage your shared documents</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{Object.entries(documents).length > 0 ? (
|
||||||
|
Object.entries(documents).map(([folderName, docs]) => (
|
||||||
|
<div
|
||||||
|
key={folderName}
|
||||||
|
className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden transition-all duration-200 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFolder(folderName)}
|
||||||
|
className="w-full px-6 py-4 flex items-center justify-between text-white hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<FiFolder className="w-5 h-5 text-blue-400" />
|
||||||
|
<span className="font-medium">{folderName}</span>
|
||||||
|
<span className="text-sm text-gray-400">({docs.length} files)</span>
|
||||||
|
</div>
|
||||||
|
<FiChevronRight
|
||||||
|
className={`w-5 h-5 transition-transform duration-200 ${
|
||||||
|
expandedFolders[folderName] ? 'rotate-90' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedFolders[folderName] && (
|
||||||
|
<div className="border-t border-gray-700">
|
||||||
|
{docs.map((doc) => (
|
||||||
|
<div
|
||||||
|
key={doc.id}
|
||||||
|
className="px-6 py-3 flex items-center justify-between hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<FiFile className="w-4 h-4 text-gray-400" />
|
||||||
|
<span className="text-gray-200">{doc.title}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownload(doc.s3Key)}
|
||||||
|
className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
|
||||||
|
title="Download document"
|
||||||
|
>
|
||||||
|
<FiDownload className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-400">No documents available</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
174
frontend/src/components/documentUpload/DocumentUpload.jsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { uploadDocument, getAllUsers, getUserInfo } from '../../services/api';
|
||||||
|
|
||||||
|
function DocumentUpload() {
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState([]);
|
||||||
|
const [availableUsers, setAvailableUsers] = useState([]);
|
||||||
|
const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
getCurrentUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getAllUsers();
|
||||||
|
setAvailableUsers(response.data.filter(user => !user.isAdmin));
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage('Failed to load users');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getUserInfo();
|
||||||
|
console.log('Current user data:', response.data); // Debug log
|
||||||
|
setCurrentUser(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get current user:', error);
|
||||||
|
setErrorMessage('Failed to get current user info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!file || !title || selectedUsers.length === 0 || !currentUser) {
|
||||||
|
setErrorMessage('Please provide all required information');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('uploading');
|
||||||
|
setErrorMessage('');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('title', title);
|
||||||
|
formData.append('sharedWithId', selectedUsers[0]); // Remove toString()
|
||||||
|
formData.append('uploadedById', currentUser.id); // Remove toString()
|
||||||
|
|
||||||
|
// Debug log
|
||||||
|
console.log('Form Data Contents:', {
|
||||||
|
file: formData.get('file'),
|
||||||
|
title: formData.get('title'),
|
||||||
|
sharedWithId: formData.get('sharedWithId'),
|
||||||
|
uploadedById: formData.get('uploadedById')
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await uploadDocument(formData);
|
||||||
|
console.log('Upload response:', response);
|
||||||
|
|
||||||
|
setStatus('completed');
|
||||||
|
setTitle('');
|
||||||
|
setFile(null);
|
||||||
|
setSelectedUsers([]);
|
||||||
|
event.target.reset();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
setStatus('failed');
|
||||||
|
setErrorMessage(error.response?.data?.message || 'Upload failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleFileChange(e) {
|
||||||
|
setFile(e.target.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserSelect = (e) => {
|
||||||
|
const values = Array.from(e.target.selectedOptions, option => option.value);
|
||||||
|
console.log('Selected user values:', values);
|
||||||
|
setSelectedUsers(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Upload Document</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
Document Title
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
File
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
className="mt-1 block w-full"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
|
Share with Users
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectedUsers}
|
||||||
|
onChange={handleUserSelect}
|
||||||
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{availableUsers.map(user => (
|
||||||
|
<option key={user.id} value={user.id}>
|
||||||
|
{user.name} ({user.email})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="text-red-500 text-sm mt-2">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={status === 'uploading'}
|
||||||
|
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white
|
||||||
|
${status === 'uploading'
|
||||||
|
? 'bg-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-indigo-600 hover:bg-indigo-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status === 'uploading' ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Uploading...
|
||||||
|
</>
|
||||||
|
) : 'Upload Document'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{status === 'completed' && (
|
||||||
|
<div className="text-green-500 text-sm mt-2">
|
||||||
|
Document uploaded successfully!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentUpload;
|
||||||
4
frontend/src/components/footer/footer.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
footer {
|
||||||
|
position: relative;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
148
frontend/src/components/footer/footer.jsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { NavLink } from 'react-router-dom'
|
||||||
|
import './footer.css'
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<footer
|
||||||
|
className="text-center text-white bg-gradient-to-b from-cyan-900 to-cyan-800 lg:text-left">
|
||||||
|
|
||||||
|
|
||||||
|
<div className="mx-6 py-10 text-center md:text-left">
|
||||||
|
<div className="grid-1 grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<div className="">
|
||||||
|
<h6
|
||||||
|
className="mb-4 flex items-center justify-center font-semibold uppercase md:justify-start">
|
||||||
|
|
||||||
|
Испитување материјали и консултантство
|
||||||
|
</h6>
|
||||||
|
<p>
|
||||||
|
Вашиот партнер во контролата на безбедно градење и квалитетно живеење
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<h6
|
||||||
|
className="mb-4 flex justify-center font-semibold uppercase md:justify-start">
|
||||||
|
Услуги
|
||||||
|
</h6>
|
||||||
|
<NavLink to={'lab'}>
|
||||||
|
<p className="mb-4">
|
||||||
|
<a href="#!" className="text-neutral-600 dark:text-neutral-200"
|
||||||
|
>Лабораториски анализи</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to={'ultrasound'}>
|
||||||
|
<p className="mb-4">
|
||||||
|
<a href="#!" className="text-neutral-600 dark:text-neutral-200"
|
||||||
|
>Испитување со ултразвук</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to={'consulting'}>
|
||||||
|
<p className="mb-4">
|
||||||
|
<a href="#!" className="text-neutral-600 dark:text-neutral-200"
|
||||||
|
>Консултатски услуги</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<h6
|
||||||
|
className="mb-4 flex justify-center font-semibold uppercase md:justify-start">
|
||||||
|
Корисни линкови
|
||||||
|
</h6>
|
||||||
|
<p className="mb-4">
|
||||||
|
<a href="#!" className="text-neutral-600 dark:text-neutral-200"
|
||||||
|
>Безбедно градење</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p className="mb-4">
|
||||||
|
<a href="#!" className="text-neutral-600 dark:text-neutral-200"
|
||||||
|
>Контрола на квалитет</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<NavLink to="/certificates" >
|
||||||
|
<p className="mb-4">
|
||||||
|
<a>Сертификати</a>
|
||||||
|
</p>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6
|
||||||
|
className="mb-4 flex justify-center font-semibold uppercase md:justify-start">
|
||||||
|
Контакт
|
||||||
|
</h6>
|
||||||
|
<p className="mb-4 flex items-center justify-center md:justify-start">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="mr-3 h-5 w-5">
|
||||||
|
<path
|
||||||
|
d="M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 101.06-1.06l-8.689-8.69a2.25 2.25 0 00-3.182 0l-8.69 8.69a.75.75 0 001.061 1.06l8.69-8.69z" />
|
||||||
|
<path
|
||||||
|
d="M12 5.432l8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 01-.75-.75v-4.5a.75.75 0 00-.75-.75h-3a.75.75 0 00-.75.75V21a.75.75 0 01-.75.75H5.625a1.875 1.875 0 01-1.875-1.875v-6.198a2.29 2.29 0 00.091-.086L12 5.43z" />
|
||||||
|
</svg>
|
||||||
|
16-та Македонска бригада бр. 18
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 flex items-center justify-center md:justify-start">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="mr-3 h-5 w-5">
|
||||||
|
<path
|
||||||
|
d="M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z" />
|
||||||
|
<path
|
||||||
|
d="M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z" />
|
||||||
|
</svg>
|
||||||
|
stanko@imk.mk
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 flex items-center justify-center md:justify-start">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="mr-3 h-5 w-5">
|
||||||
|
<path
|
||||||
|
d="M1.5 8.67v8.58a3 3 0 003 3h15a3 3 0 003-3V8.67l-8.928 5.493a3 3 0 01-3.144 0L1.5 8.67z" />
|
||||||
|
<path
|
||||||
|
d="M22.5 6.908V6.75a3 3 0 00-3-3h-15a3 3 0 00-3 3v.158l9.714 5.978a1.5 1.5 0 001.572 0L22.5 6.908z" />
|
||||||
|
</svg>
|
||||||
|
info.imkmk@gmail.com
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 flex items-center justify-center md:justify-start">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="mr-3 h-5 w-5">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M1.5 4.5a3 3 0 013-3h1.372c.86 0 1.61.586 1.819 1.42l1.105 4.423a1.875 1.875 0 01-.694 1.955l-1.293.97c-.135.101-.164.249-.126.352a11.285 11.285 0 006.697 6.697c.103.038.25.009.352-.126l.97-1.293a1.875 1.875 0 011.955-.694l4.423 1.105c.834.209 1.42.959 1.42 1.82V19.5a3 3 0 01-3 3h-2.25C8.552 22.5 1.5 15.448 1.5 6.75V4.5z"
|
||||||
|
clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
+ 389 70 279 877
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-neutral-200 p-6 text-center dark:bg-neutral-700">
|
||||||
|
<span>© 2023 Copyright:</span>
|
||||||
|
<a
|
||||||
|
className="font-semibold text-neutral-600 dark:text-neutral-400"
|
||||||
|
href="https://tailwind-elements.com/"
|
||||||
|
> IMK</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</footer >
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer
|
||||||
146
frontend/src/components/gallery/Gallery.jsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import './gallery.css'
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
// import { FiZoomIn } from 'react-icons/fi';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
function Gallery() {
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
{ id: 1, src: "1.jpg", alt: "Laboratory Equipment 1", category: "Lab" },
|
||||||
|
{ id: 2, src: "2.jpg", alt: "Laboratory Equipment 2", category: "Lab" },
|
||||||
|
{ id: 3, src: "3.jpg", alt: "Laboratory Equipment 3", category: "Lab" },
|
||||||
|
{ id: 4, src: "4.jpg", alt: "Testing Process 1", category: "Testing" },
|
||||||
|
{ id: 5, src: "5.jpg", alt: "Testing Process 2", category: "Testing" },
|
||||||
|
{ id: 6, src: "6.jpg", alt: "Testing Process 3", category: "Testing" },
|
||||||
|
{ id: 8, src: "8.jpg", alt: "Results 1", category: "Results" },
|
||||||
|
{ id: 9, src: "9.jpg", alt: "Results 2", category: "Results" },
|
||||||
|
{ id: 10, src: "10.jpg", alt: "Results 3", category: "Results" }
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
// Simulate images loading
|
||||||
|
const loadImages = async () => {
|
||||||
|
await Promise.all(
|
||||||
|
images.map(img => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = img.src;
|
||||||
|
image.onload = resolve;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
loadImages();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-b from-cyan-900 to-cyan-800 py-20">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="container mx-auto px-4 mb-16 my-20">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6">
|
||||||
|
Нашата Галерија
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Погледнете ја нашата опрема и работен процес преку слики
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
className="container mx-auto px-4"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{images.map((image) => (
|
||||||
|
<motion.div
|
||||||
|
key={image.id}
|
||||||
|
className=""
|
||||||
|
variants={itemVariants}
|
||||||
|
>
|
||||||
|
<div className="aspect-w-16 aspect-h-12 rounded-xl overflow-hidden bg-white/10 backdrop-blur-lg">
|
||||||
|
<img
|
||||||
|
src={image.src}
|
||||||
|
alt={image.alt}
|
||||||
|
className="w-full h-full object-cover transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||||
|
<p className="text-white text-sm">{image.alt}</p>
|
||||||
|
<span className="text-blue-400 text-xs">{image.category}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(image)}
|
||||||
|
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-blue-500 p-3 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
>
|
||||||
|
{/* <FiZoomIn className="w-6 h-6 text-white" /> */}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Image Modal */}
|
||||||
|
{selectedImage && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
>
|
||||||
|
<div className="relative max-w-4xl w-full">
|
||||||
|
<img
|
||||||
|
src={selectedImage.src}
|
||||||
|
alt={selectedImage.alt}
|
||||||
|
className="w-full h-auto rounded-xl"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(null)}
|
||||||
|
className="absolute top-4 right-4 text-white hover:text-blue-400"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gallery
|
||||||
18
frontend/src/components/gallery/gallery.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.aspect-w-16 {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aspect-w-16 > * {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
122
frontend/src/components/lab/lab.jsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function Lab() {
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="container mx-auto px-4 py-12">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mt-8 mb-16 text-center">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-semibold capitalize text-gray-800 mb-4">
|
||||||
|
во нашата лабораторија се вршат
|
||||||
|
</h1>
|
||||||
|
<div className="w-24 h-1 bg-blue-500 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="space-y-16">
|
||||||
|
{/* Concrete Testing Section */}
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-8 md:gap-16">
|
||||||
|
<div className="w-full md:w-1/2 p-8 bg-white rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 mb-4">
|
||||||
|
Испитувањето на бетон
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-gray-600">
|
||||||
|
<p>
|
||||||
|
е важен процес кој се користи за да се оцени квалитетот и издржливоста на бетонски конструкции.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Испитувањето на бетонот е неопходен процес за осигурување на квалитет и безбедност на градежните проекти и инфраструктурата воопшто.
|
||||||
|
Тоа помага во отстранивање на недостатоците и намалување на ризиците од евентуални пропаднувања на конструкциите.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<img
|
||||||
|
src="/kocki.webp"
|
||||||
|
alt="Испитување на бетон"
|
||||||
|
loading="lazy"
|
||||||
|
className="rounded-xl shadow-lg w-full h-[400px] object-cover hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ore and Metal Testing Section */}
|
||||||
|
<div className="flex flex-col md:flex-row-reverse items-center gap-8 md:gap-16">
|
||||||
|
<div className="w-full md:w-1/2 p-8 bg-white rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 mb-4">
|
||||||
|
Испитувањето на руди, метали и троски
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-gray-600">
|
||||||
|
<p>
|
||||||
|
е важен процес во областа на геолошкото и металуршко инженерство, со цел да се анализираат и одредат својствата и составот на суровините и производите.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Овие испитувања се изведуваат за да се осигура квалитетот и спецификациите на материјалите, како и за да се обезбеди безбедно и ефикасно користење и обработка на руди, метали и троски во различни индустрии и апликации.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<img
|
||||||
|
src="/lab.webp"
|
||||||
|
alt="Испитување на руди и метали"
|
||||||
|
loading="lazy"
|
||||||
|
className="rounded-xl shadow-lg w-full h-[400px] object-cover hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Steel Testing Section */}
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-8 md:gap-16">
|
||||||
|
<div className="w-full md:w-1/2 p-8 bg-white rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 mb-4">
|
||||||
|
Испитувањето на челик
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-gray-600">
|
||||||
|
<p>
|
||||||
|
е важен процес за одредување на квалитетот, издржливоста и други механички својства на челикот, кој се користи во разни индустрии и конструкции.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Испитувањето на челик е неопходен процес за да се обезбеди квалитетот и безбедноста на производите и конструкциите кои го користат, како и за да се уверите дека челикот одговара на спецификациите и стандардите.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<img
|
||||||
|
src="/armatura.jpg"
|
||||||
|
alt="Испитување на челик"
|
||||||
|
loading="lazy"
|
||||||
|
className="rounded-xl shadow-lg w-full h-[400px] object-cover hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Laboratory Extraction Section */}
|
||||||
|
<div className="flex flex-col md:flex-row-reverse items-center gap-8 md:gap-16">
|
||||||
|
<div className="w-full md:w-1/2 p-8 bg-white rounded-xl shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 mb-4">
|
||||||
|
Лабораториската екстракција
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-gray-600">
|
||||||
|
<p>
|
||||||
|
на метали или неметали од руди и троски е процес кој се користи за да се изолираат и оддели материјалите од суровините или отпадните производи, како што се рудите или троските, со цел да се добие чист метал или неметал.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
|
<img
|
||||||
|
src="/geotehnika.webp"
|
||||||
|
alt="Лабораториска екстракција"
|
||||||
|
loading="lazy"
|
||||||
|
className="rounded-xl shadow-lg w-full h-[400px] object-cover hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
168
frontend/src/components/login/login.jsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// import { useState } from 'react';
|
||||||
|
// import { useAuth } from '../../hooks/useAuth';
|
||||||
|
// import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
// const Login = () => {
|
||||||
|
// const [username, setUsername] = useState(''); // Changed from email to username
|
||||||
|
// const [password, setPassword] = useState('');
|
||||||
|
// const [error, setError] = useState('');
|
||||||
|
// const { login } = useAuth();
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
|
||||||
|
// const handleSubmit = async (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// setError('');
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const userData = await login(username, password);
|
||||||
|
// console.log('Login result:', userData);
|
||||||
|
// if (userData?.isAdmin) {
|
||||||
|
// navigate('/admin');
|
||||||
|
// } else {
|
||||||
|
// navigate('/dashboard');
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// setError('Invalid credentials');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
// <div className="max-w-md w-full space-y-8">
|
||||||
|
// <div>
|
||||||
|
// <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
|
// Sign in to your account
|
||||||
|
// </h2>
|
||||||
|
// </div>
|
||||||
|
// <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||||
|
// {error && (
|
||||||
|
// <div className="text-red-500 text-center text-sm">
|
||||||
|
// {error}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// <div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
// <div>
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// required
|
||||||
|
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
// placeholder="Username"
|
||||||
|
// value={username}
|
||||||
|
// onChange={(e) => setUsername(e.target.value)}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <input
|
||||||
|
// type="password"
|
||||||
|
// required
|
||||||
|
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
// placeholder="Password"
|
||||||
|
// value={password}
|
||||||
|
// onChange={(e) => setPassword(e.target.value)}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div>
|
||||||
|
// <button
|
||||||
|
// type="submit"
|
||||||
|
// className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
// >
|
||||||
|
// Sign in
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </form>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default Login;
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { FiUser, FiLock } from 'react-icons/fi'; // Import icons
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const { login } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = await login(username, password);
|
||||||
|
if (userData?.isAdmin) {
|
||||||
|
navigate('/admin');
|
||||||
|
} else {
|
||||||
|
navigate('/dashboard');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Invalid credentials');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
|
||||||
|
<div className="w-full max-w-md px-8 py-10 bg-white/5 backdrop-blur-lg rounded-2xl shadow-2xl">
|
||||||
|
<div className="mb-10 text-center">
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">Welcome Back</h1>
|
||||||
|
<p className="text-gray-400">Please sign in to continue</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 text-sm text-red-200 bg-red-500/10 border border-red-500/20 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<FiUser className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
className="w-full px-10 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400 transition-colors"
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<FiLock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
className="w-full px-10 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400 transition-colors"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Need help? Contact your administrator
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
133
frontend/src/components/navbar/Navbar.jsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Dialog } from "@headlessui/react";
|
||||||
|
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: "Дома", href: "/" },
|
||||||
|
{ name: "За нас", href: "/about" },
|
||||||
|
{ name: "Галерија", href: "/gallery" },
|
||||||
|
{ name: "Контакт", href: "/contact" },
|
||||||
|
// { name: "Клиенти", href: "/clients" },
|
||||||
|
{ name: "Админ", href: "/admin" },
|
||||||
|
{ name: "Логин", href: "/login" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute w-full z-50 py-10">
|
||||||
|
<header className="relative bg-transparent">
|
||||||
|
<nav
|
||||||
|
className="container mx-auto flex items-center justify-between p-6 lg:px-8"
|
||||||
|
aria-label="Global"
|
||||||
|
>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex lg:flex-1">
|
||||||
|
<a href="/" className="flex items-center -m-1.5 p-1.5">
|
||||||
|
<img
|
||||||
|
className="h-12 w-auto hover:scale-105 transition-transform duration-300"
|
||||||
|
src="/imklogorgb.png"
|
||||||
|
alt="IMK logo"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="flex lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-white hover:bg-white/10 transition-colors duration-300"
|
||||||
|
onClick={() => setMobileMenuOpen(true)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Open main menu</span>
|
||||||
|
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop navigation */}
|
||||||
|
<div className="hidden lg:flex lg:gap-x-12">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.name}
|
||||||
|
to={item.href}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`text-base font-semibold leading-6 transition-all duration-300
|
||||||
|
${isActive
|
||||||
|
? 'text-white border-b-2 border-white'
|
||||||
|
: 'text-gray-200 hover:text-white hover:border-b-2 hover:border-white/50'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TUF Logo */}
|
||||||
|
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
||||||
|
<a href="/" className="flex items-center -m-1.5 p-1.5">
|
||||||
|
<img
|
||||||
|
className="h-12 w-auto hover:scale-105 transition-transform duration-300"
|
||||||
|
src="/tuf.png"
|
||||||
|
alt="TUF logo"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile menu */}
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="lg:hidden"
|
||||||
|
open={mobileMenuOpen}
|
||||||
|
onClose={setMobileMenuOpen}
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 z-50" />
|
||||||
|
<Dialog.Panel className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-gradient-to-b from-blue-900 to-blue-800 px-6 py-6 sm:max-w-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<a href="/" className="-m-1.5 p-1.5">
|
||||||
|
<img
|
||||||
|
className="h-8 w-auto"
|
||||||
|
src="/imklogorgb.png"
|
||||||
|
alt="IMK logo"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="-m-2.5 rounded-md p-2.5 text-white hover:bg-white/10 transition-colors duration-300"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close menu</span>
|
||||||
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flow-root">
|
||||||
|
<div className="-my-6 divide-y divide-white/10">
|
||||||
|
<div className="space-y-2 py-6">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.name}
|
||||||
|
to={item.href}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 transition-colors duration-300
|
||||||
|
${isActive
|
||||||
|
? 'text-white bg-white/10'
|
||||||
|
: 'text-gray-200 hover:bg-white/5 hover:text-white'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Dialog>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
frontend/src/components/protectedRoute/ProtectedRoute.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const ProtectedRoute = ({ children }) => {
|
||||||
|
const { user, isLoading } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !user) {
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
}, [user, isLoading, navigate]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user ? children : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
15
frontend/src/components/ultrasound/ultraSound.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function UltraSound() {
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo({ top: -100, behavior: "smooth" })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="ultraSoundContainer flex justify-around mt-40">
|
||||||
|
<iframe width="1120" height="630" src="https://www.youtube.com/embed/WGjG8XWS9RQ?si=fE2aL5eZ-5ivuuxV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
85
frontend/src/hooks/useAuth.jsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import api from '../services/api';
|
||||||
|
|
||||||
|
const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }) => {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get('/auth/user-info'); // Updated endpoint
|
||||||
|
setUser(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user info:', error);
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
// const login = async (username, password) => {
|
||||||
|
// try {
|
||||||
|
// const response = await api.post('/auth/login', { username, password });
|
||||||
|
// const { access_token, user } = response.data; // Make sure this matches your backend response
|
||||||
|
|
||||||
|
// localStorage.setItem('token', access_token);
|
||||||
|
// setUser(user);
|
||||||
|
// return user;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Login error:', error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
const login = async (username, password) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/auth/login', { username, password });
|
||||||
|
console.log('Login response:', response.data); // Debug log
|
||||||
|
|
||||||
|
const { access_token } = response.data;
|
||||||
|
localStorage.setItem('token', access_token);
|
||||||
|
|
||||||
|
// Fetch user info after login
|
||||||
|
const userResponse = await api.get('/auth/user-info');
|
||||||
|
const userData = userResponse.data;
|
||||||
|
|
||||||
|
setUser(userData);
|
||||||
|
return userData; // Return the user data for redirect logic
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
3
frontend/src/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
10
frontend/src/main.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
131
frontend/src/pages/aboutpage/About.jsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { SectionHeader } from "../../shared/SectionHeader";
|
||||||
|
import { FiTrendingUp, FiAward, FiCheckCircle } from "react-icons/fi";
|
||||||
|
|
||||||
|
const milestones = [
|
||||||
|
{ year: "2008", title: "Основање на компанијата" },
|
||||||
|
{ year: "2012", title: "Проширување на лабораторијата" },
|
||||||
|
{ year: "2015", title: "ISO сертификација" },
|
||||||
|
{ year: "2018", title: "Воведување на нови услуги" },
|
||||||
|
{ year: "2020", title: "Модернизација на опремата" },
|
||||||
|
{ year: "2023", title: "Проширување на тимот" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function About() {
|
||||||
|
return (
|
||||||
|
<div className="isolate bg-graymin-h-screen bg-gradient-to-b from-cyan-900 to-cyan-400">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="relative h-screen overflow-hidden">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-900/90 to-blue-600/80" />
|
||||||
|
</div>
|
||||||
|
<div className="relative container mx-auto px-4 h-full flex items-center">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 1, ease: "easeInOut" }}
|
||||||
|
className="text-4xl md:text-6xl font-bold text-white mb-6"
|
||||||
|
>
|
||||||
|
За Нас
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 1, delay: 0.2, ease: "easeIn" }}
|
||||||
|
className="text-xl text-gray-200 mb-8"
|
||||||
|
>
|
||||||
|
Повеќе од 15 години искуство во обезбедување квалитет
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vision & Values Section */}
|
||||||
|
<div className="bg-white py-24">
|
||||||
|
<div className="container bg-gradient-to-b from-cyan-900 to-cyan-400 rounded-xl mx-auto p-20">
|
||||||
|
<SectionHeader
|
||||||
|
title="Нашата Мисија"
|
||||||
|
subtitle="Посветени сме на обезбедување највисок квалитет во нашата работа"
|
||||||
|
className="text-white"
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16">
|
||||||
|
<div className="bg-white/10 backdrop-blur-lg rounded-xl p-8">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<FiTrendingUp className="w-6 h-6 text-white mr-3" />
|
||||||
|
<h3 className="text-2xl font-semibold text-white">Визија</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-white text-xl">
|
||||||
|
Нашата визија е да бидеме водечка компанија во областа на
|
||||||
|
испитување на материјали и контрола на квалитет во Македонија.
|
||||||
|
Стремиме кон постојано унапредување на нашите услуги преку
|
||||||
|
имплементација на најсовремени технологии и методологии. Сакаме
|
||||||
|
да воспоставиме нови стандарди во индустријата и да допринесеме
|
||||||
|
за развојот на градежништвото и инженерството во регионот.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/10 backdrop-blur-lg rounded-xl p-8">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<FiAward className="w-6 h-6 text-white mr-3" />
|
||||||
|
<h3 className="text-2xl font-semibold text-white">Вредности</h3>
|
||||||
|
</div>
|
||||||
|
<ul className="text-gray-300 space-y-3">
|
||||||
|
<li className="flex items-start text-xl">
|
||||||
|
<FiCheckCircle className="w-5 h-5 text-red mr-2 mt-1" />
|
||||||
|
<div>
|
||||||
|
<strong className="text-white">Квалитет</strong> - Посветени
|
||||||
|
сме на обезбедување највисок квалитет во сите наши услуги
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<FiCheckCircle className="w-5 h-5 text-blue-400 mr-2 mt-1" />
|
||||||
|
<div>
|
||||||
|
<strong className="text-white">Интегритет</strong> -
|
||||||
|
Работиме со целосна транспарентност и професионална етика
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<FiCheckCircle className="w-5 h-5 text-blue-400 mr-2 mt-1" />
|
||||||
|
<div>
|
||||||
|
<strong className="text-white">Иновација</strong> -
|
||||||
|
Постојано инвестираме во нови технологии и методи
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<FiCheckCircle className="w-5 h-5 text-blue-400 mr-2 mt-1" />
|
||||||
|
<div>
|
||||||
|
<strong className="text-white">Експертиза</strong> - Нашиот
|
||||||
|
тим се состои од високо квалификувани професионалци
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<FiCheckCircle className="w-5 h-5 text-blue-400 mr-2 mt-1" />
|
||||||
|
<div>
|
||||||
|
<strong className="text-white">Одговорност</strong> -
|
||||||
|
Преземаме целосна одговорност за квалитетот на нашата работа
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<div className="isolate bg-gray bg-gradient-to-b from-blue-700 to-blue-500 text-white py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-8">
|
||||||
|
Започнете го вашиот проект со нас
|
||||||
|
</h2>
|
||||||
|
<NavLink
|
||||||
|
to="/contact"
|
||||||
|
className="inline-block bg-white text-blue-600 px-8 py-3 rounded-full font-semibold hover:bg-gray-100 transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Контактирајте нѐ
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
frontend/src/pages/cloud/Cloud.jsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const Cloud = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Cloud Page</h1>
|
||||||
|
<link rel="stylesheet" href="https://example.com/styles.css" />
|
||||||
|
{/* Rest of your code */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cloud;
|
||||||
115
frontend/src/pages/contactpage/Contact.jsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||||
|
import { Switch } from '@headlessui/react'
|
||||||
|
|
||||||
|
function classNames(...classes) {
|
||||||
|
return classes.filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Contact() {
|
||||||
|
const [agreed, setAgreed] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="isolate bg-gray px-6 bg-gradient-to-b from-cyan-900 to-cyan-800 sm:py-32 lg:px-8">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto max-w-2xl text-center mt-10">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-3xl">Пишете ни порака | Закажете средба</h2>
|
||||||
|
{/* <p className="mt-2 text-lg leading-8 text-gray-600">
|
||||||
|
take a wheel or let it slide
|
||||||
|
|
||||||
|
</p> */}
|
||||||
|
</div>
|
||||||
|
<form action="https://formsubmit.co/taratur@gmail.com" method="POST" className="mx-auto mt-16 max-w-xl sm:mt-20">
|
||||||
|
<div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="first-name" className="block text-sm font-semibold leading-6 text-white">
|
||||||
|
Име
|
||||||
|
</label>
|
||||||
|
<div className="mt-2.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="first-name"
|
||||||
|
id="first-name"
|
||||||
|
autoComplete="given-name"
|
||||||
|
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="last-name" className="block text-sm font-semibold leading-6 text-white">
|
||||||
|
Презиме
|
||||||
|
</label>
|
||||||
|
<div className="mt-2.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="last-name"
|
||||||
|
id="last-name"
|
||||||
|
autoComplete="family-name"
|
||||||
|
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="company" className="block text-sm font-semibold leading-6 text-white">
|
||||||
|
Компанија
|
||||||
|
</label>
|
||||||
|
<div className="mt-2.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="company"
|
||||||
|
id="company"
|
||||||
|
autoComplete="organization"
|
||||||
|
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="email" className="block text-sm font-semibold leading-6 text-white">
|
||||||
|
Мејл
|
||||||
|
</label>
|
||||||
|
<div className="mt-2.5">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
autoComplete="email"
|
||||||
|
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<label htmlFor="message" className="block text-sm font-semibold leading-6 text-white">
|
||||||
|
Порака
|
||||||
|
</label>
|
||||||
|
<div className="mt-2.5">
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
id="message"
|
||||||
|
rows={4}
|
||||||
|
className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
defaultValue={''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch.Group as="div" className="flex gap-x-4 sm:col-span-2">
|
||||||
|
</Switch.Group>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="_next" value="https://imk.mk/"></input>
|
||||||
|
<div className="mt-10">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="block w-full rounded-md bg-gray-500 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-gray-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500"
|
||||||
|
>
|
||||||
|
Испрати
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
217
frontend/src/pages/homepage/Home.jsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ArrowRightIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { SectionHeader } from "../../shared/SectionHeader";
|
||||||
|
const serviceCards = [
|
||||||
|
{
|
||||||
|
title: "Лабораториски услуги",
|
||||||
|
description:
|
||||||
|
"Професионални лабораториски испитувања со најсовремена опрема",
|
||||||
|
image: "labfinal.webp",
|
||||||
|
link: "/lab",
|
||||||
|
services: [
|
||||||
|
"Испитување на бетон",
|
||||||
|
"Геотехнички испитувања",
|
||||||
|
"Испитување на челик",
|
||||||
|
"Испитување на руди, метали и троски",
|
||||||
|
"Лабораториска екстракција од руди и троски",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ултразвучни испитувања",
|
||||||
|
description: "Неразорувачки испитувања со најсовремена ултразвучна опрема",
|
||||||
|
image: "wallscener.jpeg",
|
||||||
|
link: "/ultrasound",
|
||||||
|
services: [
|
||||||
|
"Испитување на заварени споеви",
|
||||||
|
"Дебелометрија",
|
||||||
|
"Испитување на дефекти",
|
||||||
|
"Контрола на квалитет",
|
||||||
|
"Анализа и известување",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Консалтинг услуги",
|
||||||
|
description: "Експертски совети и консултации во градежништвото",
|
||||||
|
image: "consulting2.jpg",
|
||||||
|
link: "/consulting",
|
||||||
|
services: [
|
||||||
|
"Градежно советување",
|
||||||
|
"Проектен менаџмент",
|
||||||
|
"Надзор на градба",
|
||||||
|
"Техничка документација",
|
||||||
|
"Анализа на материјали",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="isolate bg-graymin-h-screen bg-gradient-to-b from-cyan-900 to-cyan-400">
|
||||||
|
{/* Hero Section with Parallax */}
|
||||||
|
<div className="relative h-screen overflow-hidden">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-900/90 to-blue-600/80" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative container mx-auto px-4 h-full flex items-center">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-4xl md:text-6xl font-bold text-white mb-6"
|
||||||
|
>
|
||||||
|
Вашиот партнер во контролата на безбедно градење
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="text-xl text-gray-200 mb-8"
|
||||||
|
>
|
||||||
|
Професионални лабораториски услуги со најсовремена опрема и
|
||||||
|
експертиза
|
||||||
|
</motion.p>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
to="/contact"
|
||||||
|
className="bg-blue-500 hover:bg-blue-600 text-white px-8 py-3 rounded-full font-semibold transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Контактирајте нѐ
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/about"
|
||||||
|
className="bg-white/10 hover:bg-white/20 text-white px-8 py-3 rounded-full font-semibold transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Дознајте повеќе
|
||||||
|
</NavLink>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Section */}
|
||||||
|
{/* <div className="container mx-auto px-4 py-16 ">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
|
{[
|
||||||
|
{ number: "15+", label: "Години искуство" },
|
||||||
|
{ number: "1000+", label: "Задоволни клиенти" },
|
||||||
|
{ number: "5000+", label: "Завршени проекти" },
|
||||||
|
{ number: "100%", label: "Задоволство" },
|
||||||
|
].map((stat) => (
|
||||||
|
<div key={stat.label} className="text-center">
|
||||||
|
<div className="text-4xl font-bold text-white mb-2">{stat.number}</div>
|
||||||
|
<div className="text-white">{stat.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Services Section */}
|
||||||
|
<div className="bg-white py-24">
|
||||||
|
<div className="container bg-gradient-to-b from-cyan-900 to-cyan-400 rounded-xl mx-auto p-20">
|
||||||
|
{/* <div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||||
|
Нашите Услуги
|
||||||
|
</h2>
|
||||||
|
<div className="w-24 h-1 bg-blue-500 mx-auto"></div>
|
||||||
|
</div> */}
|
||||||
|
<SectionHeader
|
||||||
|
title="Нашите Услуги"
|
||||||
|
subtitle="Нашиот тим е спремен да ви помогне со нашите услуги"
|
||||||
|
className="text-white"
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||||
|
{serviceCards.map((card) => (
|
||||||
|
<NavLink
|
||||||
|
key={card.title}
|
||||||
|
to={card.link}
|
||||||
|
className="group bg-white rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
<div className="relative h-56 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={card.image}
|
||||||
|
alt={card.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||||
|
<h3 className="absolute bottom-4 left-4 text-xl font-semibold text-white">
|
||||||
|
{card.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-gray-600 mb-4">{card.description}</p>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{card.services.slice(0, 3).map((service, index) => (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
className="flex items-center text-gray-700"
|
||||||
|
>
|
||||||
|
<CheckCircleIcon className="h-5 w-5 text-blue-500 mr-2" />
|
||||||
|
{service}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-4 flex items-center text-blue-500 font-semibold">
|
||||||
|
Дознајте повеќе
|
||||||
|
<ArrowRightIcon className="h-5 w-5 ml-2 group-hover:translate-x-2 transition-transform duration-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Featured Projects/Clients */}
|
||||||
|
<div className="container mx-auto px-4 py-24">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||||
|
Наши Клиенти
|
||||||
|
</h2>
|
||||||
|
<div className="w-24 h-1 bg-blue-500 mx-auto mb-8"></div>
|
||||||
|
<p className="text-white text-xl max-w-2xl mx-auto">
|
||||||
|
Горди сме на довербата која ни ја укажуваат нашите клиенти
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||||
|
{/* Add client logos here */}
|
||||||
|
{[1, 2, 3, 4].map((index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-white p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/client-${index}.png`}
|
||||||
|
alt={`Client ${index}`}
|
||||||
|
className="w-full h-20 object-contain grayscale hover:grayscale-0 transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<div className="isolate bg-gray bg-gradient-to-b from-blue-700 to-blue-500 text-white py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-8">
|
||||||
|
Спремни сме да ви помогнеме во вашиот следен проект
|
||||||
|
</h2>
|
||||||
|
<NavLink
|
||||||
|
to="/contact"
|
||||||
|
className="inline-block bg-white text-blue-600 px-8 py-3 rounded-full font-semibold hover:bg-gray-100 transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Контактирајте нѐ
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
102
frontend/src/services/api.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:3000';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_URL,
|
||||||
|
// withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add authorization header to all requests
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${token}`; // Make sure this matches your backend expectation
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getSharedDocuments = (userId) => {
|
||||||
|
return api.get(`/documents/shared/${userId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getToken = () => localStorage.getItem('token');
|
||||||
|
export const downloadDocument = async (key) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/documents/download/${encodeURIComponent(key)}`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/octet-stream',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create blob URL and trigger download
|
||||||
|
const blob = new Blob([response.data]);
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// Extract filename from key
|
||||||
|
const fileName = key.split('/').pop();
|
||||||
|
link.download = fileName;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const createUser = (userData) => {
|
||||||
|
return api.post('/admin/users', {
|
||||||
|
name: userData.name,
|
||||||
|
email: userData.email,
|
||||||
|
password: userData.password,
|
||||||
|
isAdmin: userData.isAdmin
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const login = (username, password) => api.post('/auth/login', { username, password });
|
||||||
|
export const shareDocument = (documentId, userIds) => api.post(`/admin/documents/${documentId}/share`, { userIds });
|
||||||
|
export const updateDocumentStatus = (documentId, status) => api.put(`/admin/documents/${documentId}/status`, { status });
|
||||||
|
|
||||||
|
export const uploadDocument = async (formData) => {
|
||||||
|
try {
|
||||||
|
// Debug log
|
||||||
|
console.log('Sending to server:', {
|
||||||
|
title: formData.get('title'),
|
||||||
|
sharedWithId: formData.get('sharedWithId'),
|
||||||
|
uploadedById: formData.get('uploadedById')
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await api.post('/admin/documents', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error.response?.data);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const getUserInfo = () => api.get('/auth/user-info');
|
||||||
|
|
||||||
|
|
||||||
|
export const getAllDocuments = () => api.get('/admin/documents');
|
||||||
|
// export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`);
|
||||||
|
export const getAllUsers = () => api.get('/admin/users');
|
||||||
|
|
||||||
|
export default api;
|
||||||
56
frontend/src/services/xxFileUpload.jsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const FileUpload = () => {
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
const handleFileChange = (event) => {
|
||||||
|
if (event.target.files) {
|
||||||
|
setFile(event.target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('title', 'Your document title');
|
||||||
|
// Add other fields as needed
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post('/api/admin/document', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
|
setProgress(percentCompleted);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Upload successful');
|
||||||
|
setProgress(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error);
|
||||||
|
setProgress(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input type="file" onChange={handleFileChange} />
|
||||||
|
<button type="submit">Upload</button>
|
||||||
|
{progress > 0 && (
|
||||||
|
<div>
|
||||||
|
<progress value={progress} max="100" />
|
||||||
|
<p>{progress}% uploaded</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileUpload;
|
||||||
18
frontend/src/shared/Button.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Reusable button component
|
||||||
|
export function Button({ variant = 'primary', children, ...props }) {
|
||||||
|
const baseStyles = "px-8 py-3 rounded-full font-semibold transition-colors duration-300";
|
||||||
|
const variants = {
|
||||||
|
primary: "bg-blue-600 hover:bg-blue-700 text-white",
|
||||||
|
secondary: "bg-white/10 hover:bg-white/20 text-white",
|
||||||
|
outline: "border-2 border-blue-600 text-blue-600 hover:bg-blue-50"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`${baseStyles} ${variants[variant]}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
frontend/src/shared/SectionHeader.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Reusable section header component
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
export function SectionHeader({ title, subtitle, className }) {
|
||||||
|
return (
|
||||||
|
<div className={`text-center mb-16 text-white ${className}`}>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<div className="w-24 h-1 bg-blue-600 mx-auto mb-8"></div>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-xl max-w-2xl mx-auto">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
frontend/src/shared/colors.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const colors = {
|
||||||
|
primary: {
|
||||||
|
light: '#3B82F6', // blue-500
|
||||||
|
DEFAULT: '#2563EB', // blue-600
|
||||||
|
dark: '#1D4ED8', // blue-700
|
||||||
|
darker: '#1E40AF' // blue-800
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
light: '#F9FAFB', // gray-50
|
||||||
|
DEFAULT: '#F3F4F6', // gray-100
|
||||||
|
dark: '#E5E7EB' // gray-200
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#1F2937', // gray-800
|
||||||
|
secondary: '#4B5563', // gray-600
|
||||||
|
light: '#9CA3AF' // gray-400
|
||||||
|
},
|
||||||
|
white: '#FFFFFF',
|
||||||
|
black: '#000000'
|
||||||
|
}
|
||||||
25
frontend/src/shared/common.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export const commonStyles = {
|
||||||
|
// Container
|
||||||
|
container: "container mx-auto px-4",
|
||||||
|
|
||||||
|
// Section Headers
|
||||||
|
sectionHeader: "text-center mb-16",
|
||||||
|
sectionTitle: "text-3xl md:text-4xl font-bold text-gray-800 mb-4",
|
||||||
|
sectionDivider: "w-24 h-1 bg-blue-600 mx-auto",
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
card: "bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300",
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
primaryButton: "bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-full font-semibold transition-colors duration-300",
|
||||||
|
secondaryButton: "bg-white/10 hover:bg-white/20 text-white px-8 py-3 rounded-full font-semibold transition-colors duration-300",
|
||||||
|
|
||||||
|
// Gradients
|
||||||
|
primaryGradient: "bg-gradient-to-r from-blue-900/90 to-blue-600/80",
|
||||||
|
|
||||||
|
// Text
|
||||||
|
heading1: "text-4xl md:text-6xl font-bold",
|
||||||
|
heading2: "text-3xl md:text-4xl font-bold",
|
||||||
|
heading3: "text-2xl font-semibold",
|
||||||
|
paragraph: "text-gray-600"
|
||||||
|
}
|
||||||
8
frontend/tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
7
frontend/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||