final version

complete redesign
This commit is contained in:
dimitar 2025-02-24 23:40:35 +01:00
parent 6553b1c880
commit c7d4b95631
78 changed files with 7105 additions and 0 deletions

9
frontend/README.md Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

37
frontend/package.json Normal file
View 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"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
frontend/public/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
frontend/public/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
frontend/public/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
frontend/public/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
frontend/public/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
frontend/public/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
frontend/public/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
frontend/public/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
frontend/public/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
frontend/public/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
frontend/public/kinenje.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
frontend/public/kocka.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
frontend/public/kocki.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
frontend/public/lab.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
frontend/public/lab.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
frontend/public/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
frontend/public/naum1.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

BIN
frontend/public/naum2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

BIN
frontend/public/profili.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
frontend/public/tuf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

0
frontend/src/App.css Normal file
View File

60
frontend/src/App.jsx Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
frontend/src/assets/lab.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

View 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

View 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;

View 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;

View 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;

View 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>
</>
)
}

View 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;

View 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;

View File

@ -0,0 +1,4 @@
footer {
position: relative;
bottom: 0;
}

View 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

View 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

View 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;
}

View 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>
);
}

View 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;

View 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>
);
}

View 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;

View 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>
</>
)
}

View 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
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

10
frontend/src/main.jsx Normal file
View 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>,
)

View 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>
);
}

View 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;

View 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>
)
}

View 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>
);
}

View 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;

View 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;

View 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>
);
}

View 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>
);
}

View 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'
}

View 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"
}

View 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
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})