Spaces:
Sleeping
Sleeping
Upload 27 files
Browse files- .dockerignore +8 -0
- Dockerfile +29 -0
- Dockerfile.dev +20 -0
- index.html +30 -0
- package-lock.json +0 -0
- package.json +27 -13
- postcss.config.js +6 -0
- public/spaces-config.json +10 -0
- public/spaces-landing.html +108 -0
- src/App.css +31 -0
- src/App.jsx +12 -0
- src/components/DatasetsSection.jsx +107 -0
- src/components/NewsSection.jsx +101 -0
- src/hooks/useTheme.js +27 -0
- src/index.css +153 -0
- src/layouts/MainLayout.jsx +141 -0
- src/main.jsx +49 -0
- src/pages/DatasetsPage.jsx +201 -0
- src/pages/DirectoryPage.jsx +188 -0
- src/pages/HomePage.jsx +107 -0
- src/pages/MatchmakingPage.jsx +247 -0
- src/pages/NewsDetailPage.jsx +142 -0
- src/pages/NewsPage.jsx +123 -0
- src/pages/NotFoundPage.jsx +32 -0
- src/utils/theme.js +24 -0
- tailwind.config.js +20 -0
- vite.config.js +22 -0
.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.env
|
| 4 |
+
.git
|
| 5 |
+
.gitignore
|
| 6 |
+
README.md
|
| 7 |
+
.dockerignore
|
| 8 |
+
Dockerfile.dev
|
Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Node.js runtime
|
| 2 |
+
FROM node:18-alpine as build
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy package files
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN npm ci
|
| 12 |
+
|
| 13 |
+
# Copy application code
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Build the application
|
| 17 |
+
RUN npm run build
|
| 18 |
+
|
| 19 |
+
# Use nginx to serve the static files
|
| 20 |
+
FROM nginx:alpine
|
| 21 |
+
|
| 22 |
+
# Copy built files to nginx
|
| 23 |
+
COPY --from=build /app/dist /usr/share/nginx/html
|
| 24 |
+
|
| 25 |
+
# Expose port
|
| 26 |
+
EXPOSE 80
|
| 27 |
+
|
| 28 |
+
# Start nginx
|
| 29 |
+
CMD ["nginx", "-g", "daemon off;"]
|
Dockerfile.dev
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Node.js runtime
|
| 2 |
+
FROM node:18-alpine
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy package files
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# Install all dependencies
|
| 11 |
+
RUN npm install
|
| 12 |
+
|
| 13 |
+
# Copy application code
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Expose port
|
| 17 |
+
EXPOSE 5173
|
| 18 |
+
|
| 19 |
+
# Start the development server
|
| 20 |
+
CMD ["npm", "run", "dev"]
|
index.html
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>BioNexus Hub - Bioeconomy Innovation & FLW Nexus</title>
|
| 8 |
+
<meta name="description" content="A lightweight, regional-to-global digital nexus that connects policymakers, companies, researchers, civil society and communities to accelerate bioeconomy solutions that reduce food loss & waste (FLW) and catalyse circular agrifood-system transformation.">
|
| 9 |
+
<meta name="keywords" content="bioeconomy, food loss, waste reduction, agrifood, circular economy, sustainability">
|
| 10 |
+
<meta name="author" content="BioNexus Hub">
|
| 11 |
+
|
| 12 |
+
<!-- Open Graph / Facebook -->
|
| 13 |
+
<meta property="og:type" content="website">
|
| 14 |
+
<meta property="og:url" content="https://bionexus-hub.hf.space/">
|
| 15 |
+
<meta property="og:title" content="BioNexus Hub - Bioeconomy Innovation & FLW Nexus">
|
| 16 |
+
<meta property="og:description" content="Connecting stakeholders to accelerate bioeconomy solutions that reduce food loss & waste.">
|
| 17 |
+
<meta property="og:image" content="/vite.svg">
|
| 18 |
+
|
| 19 |
+
<!-- Twitter -->
|
| 20 |
+
<meta property="twitter:card" content="summary_large_image">
|
| 21 |
+
<meta property="twitter:url" content="https://bionexus-hub.hf.space/">
|
| 22 |
+
<meta property="twitter:title" content="BioNexus Hub - Bioeconomy Innovation & FLW Nexus">
|
| 23 |
+
<meta property="twitter:description" content="Connecting stakeholders to accelerate bioeconomy solutions that reduce food loss & waste.">
|
| 24 |
+
<meta property="twitter:image" content="/vite.svg">
|
| 25 |
+
</head>
|
| 26 |
+
<body>
|
| 27 |
+
<div id="root"></div>
|
| 28 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 29 |
+
</body>
|
| 30 |
+
</html>
|
package-lock.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -1,18 +1,32 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
-
"
|
| 4 |
-
"
|
| 5 |
-
"
|
| 6 |
"scripts": {
|
| 7 |
-
"dev": "
|
| 8 |
-
"
|
| 9 |
-
"
|
| 10 |
-
"
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
| 13 |
},
|
| 14 |
-
"private": true,
|
| 15 |
"devDependencies": {
|
| 16 |
-
"
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "bionexus-hub-client",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^18.2.0",
|
| 14 |
+
"react-dom": "^18.2.0",
|
| 15 |
+
"react-router-dom": "^6.18.0"
|
| 16 |
},
|
|
|
|
| 17 |
"devDependencies": {
|
| 18 |
+
"@tailwindcss/postcss": "^4.0.0",
|
| 19 |
+
"@types/react": "^18.2.43",
|
| 20 |
+
"@types/react-dom": "^18.2.17",
|
| 21 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 22 |
+
"autoprefixer": "^10.4.16",
|
| 23 |
+
"eslint": "^8.55.0",
|
| 24 |
+
"eslint-plugin-react": "^7.33.2",
|
| 25 |
+
"eslint-plugin-react-hooks": "^4.6.0",
|
| 26 |
+
"eslint-plugin-react-refresh": "^0.4.5",
|
| 27 |
+
"postcss": "^8.4.32",
|
| 28 |
+
"tailwindcss": "^4.0.0",
|
| 29 |
+
"vite": "^5.0.8"
|
| 30 |
+
},
|
| 31 |
+
"proxy": "http://localhost:3001"
|
| 32 |
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
'@tailwindcss/postcss': {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
}
|
| 6 |
+
}
|
public/spaces-config.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sdk": "docker",
|
| 3 |
+
"app_port": 80,
|
| 4 |
+
"container_runtime": "runc",
|
| 5 |
+
"resources": {
|
| 6 |
+
"memory": "4Gi",
|
| 7 |
+
"cpu": "2",
|
| 8 |
+
"gpu": false
|
| 9 |
+
}
|
| 10 |
+
}
|
public/spaces-landing.html
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>BioNexus Hub - Hugging Face Spaces</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
background: linear-gradient(135deg, #16a34a, #2563eb, #9333ea);
|
| 13 |
+
color: white;
|
| 14 |
+
min-height: 100vh;
|
| 15 |
+
display: flex;
|
| 16 |
+
flex-direction: column;
|
| 17 |
+
align-items: center;
|
| 18 |
+
justify-content: center;
|
| 19 |
+
text-align: center;
|
| 20 |
+
}
|
| 21 |
+
.container {
|
| 22 |
+
max-width: 800px;
|
| 23 |
+
padding: 2rem;
|
| 24 |
+
background: rgba(0, 0, 0, 0.5);
|
| 25 |
+
border-radius: 20px;
|
| 26 |
+
backdrop-filter: blur(10px);
|
| 27 |
+
margin: 1rem;
|
| 28 |
+
}
|
| 29 |
+
h1 {
|
| 30 |
+
font-size: 3rem;
|
| 31 |
+
margin-bottom: 1rem;
|
| 32 |
+
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
| 33 |
+
}
|
| 34 |
+
p {
|
| 35 |
+
font-size: 1.2rem;
|
| 36 |
+
margin-bottom: 2rem;
|
| 37 |
+
line-height: 1.6;
|
| 38 |
+
}
|
| 39 |
+
.btn {
|
| 40 |
+
display: inline-block;
|
| 41 |
+
padding: 1rem 2rem;
|
| 42 |
+
background: white;
|
| 43 |
+
color: #16a34a;
|
| 44 |
+
font-weight: bold;
|
| 45 |
+
font-size: 1.2rem;
|
| 46 |
+
border-radius: 50px;
|
| 47 |
+
text-decoration: none;
|
| 48 |
+
margin: 1rem;
|
| 49 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
| 50 |
+
transition: all 0.3s ease;
|
| 51 |
+
}
|
| 52 |
+
.btn:hover {
|
| 53 |
+
transform: translateY(-3px);
|
| 54 |
+
box-shadow: 0 6px 12px rgba(0,0,0,0.4);
|
| 55 |
+
}
|
| 56 |
+
.features {
|
| 57 |
+
display: flex;
|
| 58 |
+
flex-wrap: wrap;
|
| 59 |
+
justify-content: center;
|
| 60 |
+
gap: 1rem;
|
| 61 |
+
margin: 2rem 0;
|
| 62 |
+
}
|
| 63 |
+
.feature {
|
| 64 |
+
background: rgba(255, 255, 255, 0.1);
|
| 65 |
+
padding: 1rem;
|
| 66 |
+
border-radius: 10px;
|
| 67 |
+
flex: 1;
|
| 68 |
+
min-width: 200px;
|
| 69 |
+
}
|
| 70 |
+
.feature h3 {
|
| 71 |
+
margin-top: 0;
|
| 72 |
+
}
|
| 73 |
+
footer {
|
| 74 |
+
margin-top: 2rem;
|
| 75 |
+
font-size: 0.9rem;
|
| 76 |
+
opacity: 0.8;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<h1>BioNexus Hub</h1>
|
| 83 |
+
<p>A lightweight, regional-to-global digital nexus that connects policymakers, companies, researchers, civil society and communities to accelerate bioeconomy solutions that reduce food loss & waste (FLW).</p>
|
| 84 |
+
|
| 85 |
+
<div class="features">
|
| 86 |
+
<div class="feature">
|
| 87 |
+
<h3>📰 News Hub</h3>
|
| 88 |
+
<p>Discover regional policy updates and best practices</p>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="feature">
|
| 91 |
+
<h3>📊 Open Data</h3>
|
| 92 |
+
<p>Access datasets on cold-chain gaps and processing capacity</p>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="feature">
|
| 95 |
+
<h3>🤝 Matchmaking</h3>
|
| 96 |
+
<p>Connect with partners and funders for pilot projects</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<a href="/app" class="btn">Launch BioNexus Hub</a>
|
| 101 |
+
|
| 102 |
+
<footer>
|
| 103 |
+
<p>BioNexus Hub - Accelerating bioeconomy solutions for food loss & waste reduction</p>
|
| 104 |
+
<p>© 2025 BioNexus Hub. All rights reserved.</p>
|
| 105 |
+
</footer>
|
| 106 |
+
</div>
|
| 107 |
+
</body>
|
| 108 |
+
</html>
|
src/App.css
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* We'll primarily use Tailwind classes instead of custom CSS */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
| 3 |
+
|
| 4 |
+
.App {
|
| 5 |
+
text-align: center;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.App-header {
|
| 9 |
+
background-color: #282c34;
|
| 10 |
+
padding: 20px;
|
| 11 |
+
color: white;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.hero {
|
| 15 |
+
padding: 40px;
|
| 16 |
+
background: linear-gradient(135deg, #16a34a, #2563eb);
|
| 17 |
+
color: white;
|
| 18 |
+
border-radius: 10px;
|
| 19 |
+
margin: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.hero h2 {
|
| 23 |
+
font-size: 2rem;
|
| 24 |
+
margin-bottom: 15px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
@media (max-width: 768px) {
|
| 28 |
+
.hero h2 {
|
| 29 |
+
font-size: 1.5rem;
|
| 30 |
+
}
|
| 31 |
+
}
|
src/App.jsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
importReact from 'react'
|
| 2 |
+
|
| 3 |
+
function App() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="App">
|
| 6 |
+
<h1>GXS BioNexus Hub</h1>
|
| 7 |
+
<p>If you see this message, React Router is not working properly.</p>
|
| 8 |
+
</div>
|
| 9 |
+
)
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default App
|
src/components/DatasetsSection.jsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const DatasetsSection = () => {
|
| 5 |
+
const [datasets, setDatasets] = useState([]);
|
| 6 |
+
const [loading, setLoading] = useState(true);
|
| 7 |
+
const [error, setError] = useState(null);
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
// In a real app, this would fetch from an API
|
| 11 |
+
// For demo purposes, we'll use mock data
|
| 12 |
+
const mockDatasets = [
|
| 13 |
+
{
|
| 14 |
+
id: 1,
|
| 15 |
+
title: 'Cold Chain Infrastructure Map',
|
| 16 |
+
description: 'Geospatial data on refrigeration facilities across Sub-Saharan Africa',
|
| 17 |
+
category: 'Infrastructure',
|
| 18 |
+
size: '2.4 GB',
|
| 19 |
+
downloads: 1240
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
id: 2,
|
| 23 |
+
title: 'Post-Harvest Loss Hotspots',
|
| 24 |
+
description: 'Identified areas with highest food loss rates in South Asia',
|
| 25 |
+
category: 'Analytics',
|
| 26 |
+
size: '890 MB',
|
| 27 |
+
downloads: 890
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
id: 3,
|
| 31 |
+
title: 'Processing Capacity Database',
|
| 32 |
+
description: 'List of food processing facilities with available capacity',
|
| 33 |
+
category: 'Industry',
|
| 34 |
+
size: '1.1 GB',
|
| 35 |
+
downloads: 1560
|
| 36 |
+
}
|
| 37 |
+
];
|
| 38 |
+
|
| 39 |
+
setTimeout(() => {
|
| 40 |
+
setDatasets(mockDatasets);
|
| 41 |
+
setLoading(false);
|
| 42 |
+
}, 500);
|
| 43 |
+
}, []);
|
| 44 |
+
|
| 45 |
+
if (loading) {
|
| 46 |
+
return (
|
| 47 |
+
<section className="py-16 bg-gradient-to-br from-white to-gray-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300">
|
| 48 |
+
<div className="container mx-auto px-4">
|
| 49 |
+
<h2 className="text-3xl font-bold text-center mb-12 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Open Datasets</h2>
|
| 50 |
+
<div className="flex justify-center">
|
| 51 |
+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-bio-green"></div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</section>
|
| 55 |
+
);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if (error) {
|
| 59 |
+
return (
|
| 60 |
+
<section className="py-16 bg-gradient-to-br from-white to-gray-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300">
|
| 61 |
+
<div className="container mx-auto px-4">
|
| 62 |
+
<h2 className="text-3xl font-bold text-center mb-12 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Open Datasets</h2>
|
| 63 |
+
<div className="text-center text-red-500">
|
| 64 |
+
<p>Error loading datasets: {error}</p>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</section>
|
| 68 |
+
);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
return (
|
| 72 |
+
<section className="py-16 bg-gradient-to-br from-white to-gray-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300">
|
| 73 |
+
<div className="container mx-auto px-4">
|
| 74 |
+
<div className="flex justify-between items-center mb-12">
|
| 75 |
+
<h2 className="text-3xl font-bold text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Open Datasets</h2>
|
| 76 |
+
<Link to="/datasets" className="text-bio-blue dark:text-blue-400 hover:underline font-medium drop-shadow bg-gradient-to-r from-bio-blue to-blue-500 bg-clip-text text-transparent">
|
| 77 |
+
Browse All Datasets →
|
| 78 |
+
</Link>
|
| 79 |
+
</div>
|
| 80 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 81 |
+
{datasets.map((dataset) => (
|
| 82 |
+
<div key={dataset.id} className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg p-6 border border-blue-100 dark:border-gray-600 transition-all duration-300 transform hover:-translate-y-1 hover:shadow-xl bg-gradient-to-br from-blue-50/50 to-purple-50/50 dark:from-gray-700/50 dark:to-gray-800/50">
|
| 83 |
+
<div className="flex justify-between items-start mb-4">
|
| 84 |
+
<h3 className="text-xl font-bold text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">{dataset.title}</h3>
|
| 85 |
+
<span className="inline-block px-3 py-1 text-xs font-semibold text-bio-blue bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 rounded-full drop-shadow bg-gradient-to-r from-blue-100 to-blue-200 dark:from-blue-900/30 dark:to-blue-800/30">
|
| 86 |
+
{dataset.category}
|
| 87 |
+
</span>
|
| 88 |
+
</div>
|
| 89 |
+
<p className="text-gray-600 dark:text-gray-300 mb-4 drop-shadow">{dataset.description}</p>
|
| 90 |
+
<div className="flex justify-between items-center">
|
| 91 |
+
<div className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">
|
| 92 |
+
<span className="mr-4">Size: {dataset.size}</span>
|
| 93 |
+
<span>{dataset.downloads} downloads</span>
|
| 94 |
+
</div>
|
| 95 |
+
<button className="bg-gradient-to-r from-bio-blue to-blue-500 text-white px-4 py-2 rounded-lg hover:from-blue-500 hover:to-blue-600 transition shadow-md hover:shadow-lg drop-shadow">
|
| 96 |
+
Download
|
| 97 |
+
</button>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
))}
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</section>
|
| 104 |
+
);
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
export default DatasetsSection;
|
src/components/NewsSection.jsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const NewsSection = () => {
|
| 5 |
+
const [news, setNews] = useState([]);
|
| 6 |
+
const [loading, setLoading] = useState(true);
|
| 7 |
+
const [error, setError] = useState(null);
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
// In a real app, this would fetch from an API
|
| 11 |
+
// For demo purposes, we'll use mock data
|
| 12 |
+
const mockNews = [
|
| 13 |
+
{
|
| 14 |
+
id: 1,
|
| 15 |
+
title: 'New Cold Chain Initiative Launched in Southeast Asia',
|
| 16 |
+
excerpt: 'Regional partnership aims to reduce post-harvest losses by 30% through solar-powered refrigeration.',
|
| 17 |
+
date: '2025-10-15',
|
| 18 |
+
category: 'Policy'
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
id: 2,
|
| 22 |
+
title: 'Innovative Edible Coatings Show Promise in Lab Trials',
|
| 23 |
+
excerpt: 'New biodegradable coatings extend shelf life of fruits by up to 2 weeks.',
|
| 24 |
+
date: '2025-10-10',
|
| 25 |
+
category: 'Technology'
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
id: 3,
|
| 29 |
+
title: 'Global Fund Announces $50M for FLW Reduction Projects',
|
| 30 |
+
excerpt: 'Funding opportunity for pilot projects connecting smallholders to processing facilities.',
|
| 31 |
+
date: '2025-10-05',
|
| 32 |
+
category: 'Finance'
|
| 33 |
+
}
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
setTimeout(() => {
|
| 37 |
+
setNews(mockNews);
|
| 38 |
+
setLoading(false);
|
| 39 |
+
}, 500);
|
| 40 |
+
}, []);
|
| 41 |
+
|
| 42 |
+
if (loading) {
|
| 43 |
+
return (
|
| 44 |
+
<section className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300">
|
| 45 |
+
<div className="container mx-auto px-4">
|
| 46 |
+
<h2 className="text-3xl font-bold text-center mb-12 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Latest News & Updates</h2>
|
| 47 |
+
<div className="flex justify-center">
|
| 48 |
+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-bio-green"></div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</section>
|
| 52 |
+
);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
if (error) {
|
| 56 |
+
return (
|
| 57 |
+
<section className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300">
|
| 58 |
+
<div className="container mx-auto px-4">
|
| 59 |
+
<h2 className="text-3xl font-bold text-center mb-12 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Latest News & Updates</h2>
|
| 60 |
+
<div className="text-center text-red-500">
|
| 61 |
+
<p>Error loading news: {error}</p>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
</section>
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
return (
|
| 69 |
+
<section className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300">
|
| 70 |
+
<div className="container mx-auto px-4">
|
| 71 |
+
<div className="flex justify-between items-center mb-12">
|
| 72 |
+
<h2 className="text-3xl font-bold text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Latest News & Updates</h2>
|
| 73 |
+
<Link to="/news" className="text-bio-blue dark:text-blue-400 hover:underline font-medium drop-shadow bg-gradient-to-r from-bio-blue to-blue-500 bg-clip-text text-transparent">
|
| 74 |
+
View All News →
|
| 75 |
+
</Link>
|
| 76 |
+
</div>
|
| 77 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 78 |
+
{news.map((item) => (
|
| 79 |
+
<div key={item.id} className="bg-white dark:bg-gray-700 rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 border border-gray-100 dark:border-gray-600 bg-gradient-to-br from-white to-gray-100 dark:from-gray-700 dark:to-gray-800">
|
| 80 |
+
<div className="p-6">
|
| 81 |
+
<div className="flex justify-between items-start mb-4">
|
| 82 |
+
<span className="inline-block px-3 py-1 text-xs font-semibold text-bio-green bg-green-100 dark:bg-green-900/30 dark:text-green-300 rounded-full drop-shadow bg-gradient-to-r from-green-100 to-green-200 dark:from-green-900/30 dark:to-green-800/30">
|
| 83 |
+
{item.category}
|
| 84 |
+
</span>
|
| 85 |
+
<span className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">{new Date(item.date).toLocaleDateString()}</span>
|
| 86 |
+
</div>
|
| 87 |
+
<h3 className="text-xl font-bold mb-3 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">{item.title}</h3>
|
| 88 |
+
<p className="text-gray-600 dark:text-gray-300 mb-4 drop-shadow">{item.excerpt}</p>
|
| 89 |
+
<Link to={`/news/${item.id}`} className="text-bio-blue dark:text-blue-400 font-medium hover:text-blue-700 dark:hover:text-blue-300 transition drop-shadow bg-gradient-to-r from-bio-blue to-blue-500 bg-clip-text text-transparent">
|
| 90 |
+
Read More →
|
| 91 |
+
</Link>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
))}
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</section>
|
| 98 |
+
);
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
export default NewsSection;
|
src/hooks/useTheme.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import { applyTheme, getInitialTheme } from '../utils/theme';
|
| 3 |
+
|
| 4 |
+
export const useTheme = () => {
|
| 5 |
+
const [theme, setTheme] = useState('light');
|
| 6 |
+
|
| 7 |
+
// Check for saved theme preference or respect OS preference
|
| 8 |
+
useEffect(() => {
|
| 9 |
+
const initialTheme = getInitialTheme();
|
| 10 |
+
setTheme(initialTheme);
|
| 11 |
+
applyTheme(initialTheme);
|
| 12 |
+
}, []);
|
| 13 |
+
|
| 14 |
+
// Apply theme to document
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
applyTheme(theme);
|
| 17 |
+
}, [theme]);
|
| 18 |
+
|
| 19 |
+
const toggleTheme = () => {
|
| 20 |
+
setTheme(prevTheme => {
|
| 21 |
+
const newTheme = prevTheme === 'light' ? 'dark' : 'light';
|
| 22 |
+
return newTheme;
|
| 23 |
+
});
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
return { theme, toggleTheme };
|
| 27 |
+
};
|
src/index.css
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import 'tailwindcss';
|
| 2 |
+
|
| 3 |
+
@theme {
|
| 4 |
+
--bio-green: #16a34a;
|
| 5 |
+
--bio-blue: #2563eb;
|
| 6 |
+
--bio-purple: #9333ea;
|
| 7 |
+
--bio-orange: #ea580c;
|
| 8 |
+
--bio-yellow: #ca8a04;
|
| 9 |
+
--bio-pink: #db2777;
|
| 10 |
+
--bio-teal: #0d9488;
|
| 11 |
+
--bio-cyan: #0891b2;
|
| 12 |
+
--bio-lime: #65a30d;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@tailwind base;
|
| 16 |
+
@tailwind components;
|
| 17 |
+
@tailwind utilities;
|
| 18 |
+
|
| 19 |
+
:root {
|
| 20 |
+
font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
|
| 21 |
+
line-height: 1.5;
|
| 22 |
+
font-weight: 400;
|
| 23 |
+
|
| 24 |
+
color-scheme: light dark;
|
| 25 |
+
color: #334155;
|
| 26 |
+
background-color: #f8fafc;
|
| 27 |
+
|
| 28 |
+
font-synthesis: none;
|
| 29 |
+
text-rendering: optimizeLegibility;
|
| 30 |
+
-webkit-font-smoothing: antialiased;
|
| 31 |
+
-moz-osx-font-smoothing: grayscale;
|
| 32 |
+
-webkit-text-size-adjust: 100%;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
body {
|
| 36 |
+
margin: 0;
|
| 37 |
+
min-width: 320px;
|
| 38 |
+
min-height: 100vh;
|
| 39 |
+
transition: background-color 0.3s, color 0.3s;
|
| 40 |
+
|
| 41 |
+
/* Enhanced font rendering */
|
| 42 |
+
-webkit-font-smoothing: antialiased;
|
| 43 |
+
-moz-osx-font-smoothing: grayscale;
|
| 44 |
+
text-rendering: optimizeLegibility;
|
| 45 |
+
|
| 46 |
+
/* Vibrant background gradient */
|
| 47 |
+
background: linear-gradient(135deg, #f0f9ff 0%, #fdf2f8 50%, #f0fdf4 100%);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.dark body {
|
| 51 |
+
background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 50%, #052e16 100%);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
h1, h2, h3, h4, h5, h6 {
|
| 55 |
+
/* Improved font rendering for headings */
|
| 56 |
+
-webkit-font-smoothing: antialiased;
|
| 57 |
+
-moz-osx-font-smoothing: grayscale;
|
| 58 |
+
text-rendering: optimizeLegibility;
|
| 59 |
+
font-weight: 800;
|
| 60 |
+
|
| 61 |
+
/* Gradient text for headings */
|
| 62 |
+
background: linear-gradient(90deg, var(--bio-green), var(--bio-blue), var(--bio-purple));
|
| 63 |
+
-webkit-background-clip: text;
|
| 64 |
+
-webkit-text-fill-color: transparent;
|
| 65 |
+
background-clip: text;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.dark h1, .dark h2, .dark h3, .dark h4, .dark h5, .dark h6 {
|
| 69 |
+
background: linear-gradient(90deg, #4ade80, #60a5fa, #c084fc);
|
| 70 |
+
-webkit-background-clip: text;
|
| 71 |
+
-webkit-text-fill-color: transparent;
|
| 72 |
+
background-clip: text;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
button, a {
|
| 76 |
+
/* Improved font rendering for interactive elements */
|
| 77 |
+
-webkit-font-smoothing: antialiased;
|
| 78 |
+
-moz-osx-font-smoothing: grayscale;
|
| 79 |
+
text-rendering: optimizeLegibility;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
button {
|
| 83 |
+
border-radius: 8px;
|
| 84 |
+
border: 1px solid transparent;
|
| 85 |
+
padding: 0.6em 1.2em;
|
| 86 |
+
font-size: 1em;
|
| 87 |
+
font-weight: 500;
|
| 88 |
+
font-family: inherit;
|
| 89 |
+
background-color: #1a1a1a;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
transition: border-color 0.25s;
|
| 92 |
+
}
|
| 93 |
+
button:hover {
|
| 94 |
+
border-color: #646cff;
|
| 95 |
+
}
|
| 96 |
+
button:focus,
|
| 97 |
+
button:focus-visible {
|
| 98 |
+
outline: 4px auto -webkit-focus-ring-color;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* Dark mode styles */
|
| 102 |
+
.dark {
|
| 103 |
+
color: #f1f5f9;
|
| 104 |
+
background-color: #0f172a;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Improved font rendering for all elements */
|
| 108 |
+
* {
|
| 109 |
+
-webkit-font-smoothing: antialiased;
|
| 110 |
+
-moz-osx-font-smoothing: grayscale;
|
| 111 |
+
text-rendering: optimizeLegibility;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* Gradient text utility class */
|
| 115 |
+
.text-gradient {
|
| 116 |
+
background: linear-gradient(90deg, var(--bio-green), var(--bio-blue), var(--bio-purple));
|
| 117 |
+
-webkit-background-clip: text;
|
| 118 |
+
-webkit-text-fill-color: transparent;
|
| 119 |
+
background-clip: text;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.dark .text-gradient {
|
| 123 |
+
background: linear-gradient(90deg, #4ade80, #60a5fa, #c084fc);
|
| 124 |
+
-webkit-background-clip: text;
|
| 125 |
+
-webkit-text-fill-color: transparent;
|
| 126 |
+
background-clip: text;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* Gradient backgrounds */
|
| 130 |
+
.bg-gradient-bio {
|
| 131 |
+
background: linear-gradient(135deg, var(--bio-green), var(--bio-blue), var(--bio-purple));
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.bg-gradient-bio-alt {
|
| 135 |
+
background: linear-gradient(135deg, var(--bio-pink), var(--bio-orange), var(--bio-yellow));
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.bg-gradient-bio-light {
|
| 139 |
+
background: linear-gradient(135deg, var(--bio-cyan), var(--bio-teal), var(--bio-lime));
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
@media (prefers-color-scheme: light) {
|
| 143 |
+
:root {
|
| 144 |
+
color: #213547;
|
| 145 |
+
background-color: #ffffff;
|
| 146 |
+
}
|
| 147 |
+
a:hover {
|
| 148 |
+
color: #747bff;
|
| 149 |
+
}
|
| 150 |
+
button {
|
| 151 |
+
background-color: #f9f9f9;
|
| 152 |
+
}
|
| 153 |
+
}
|
src/layouts/MainLayout.jsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Link, useLocation } from 'react-router-dom';
|
| 3 |
+
import { useTheme } from '../hooks/useTheme';
|
| 4 |
+
|
| 5 |
+
const MainLayout = ({ children }) => {
|
| 6 |
+
const { theme, toggleTheme } = useTheme();
|
| 7 |
+
const location = useLocation();
|
| 8 |
+
|
| 9 |
+
const navigation = [
|
| 10 |
+
{ name: 'Home', path: '/' },
|
| 11 |
+
{ name: 'News', path: '/news' },
|
| 12 |
+
{ name: 'Datasets', path: '/datasets' },
|
| 13 |
+
{ name: 'Directory', path: '/directory' },
|
| 14 |
+
{ name: 'Matchmaking', path: '/matchmaking' }
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
return (
|
| 18 |
+
<div className="min-h-screen bg-gradient-to-br from-green-50 via-blue-50 to-purple-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 transition-colors duration-300">
|
| 19 |
+
<header className="bg-gradient-to-r from-bio-green via-bio-blue to-bio-purple text-white py-6 px-4 shadow-xl dark:from-gray-800 dark:via-gray-900 dark:to-gray-800 transition-colors duration-300">
|
| 20 |
+
<div className="container mx-auto flex justify-between items-center">
|
| 21 |
+
<div>
|
| 22 |
+
<Link to="/" className="text-3xl md:text-4xl font-bold hover:opacity-90 transition drop-shadow-lg">
|
| 23 |
+
<span className="bg-gradient-to-r from-yellow-300 via-green-300 to-blue-300 bg-clip-text text-transparent">
|
| 24 |
+
GXS BioNexus Hub
|
| 25 |
+
</span>
|
| 26 |
+
</Link>
|
| 27 |
+
<p className="text-lg mt-1 drop-shadow">
|
| 28 |
+
<span className="bg-gradient-to-r from-green-200 to-blue-200 bg-clip-text text-transparent">
|
| 29 |
+
Bioeconomy Innovation & FLW Nexus
|
| 30 |
+
</span>
|
| 31 |
+
</p>
|
| 32 |
+
</div>
|
| 33 |
+
<div className="flex items-center space-x-4">
|
| 34 |
+
<nav className="hidden md:block">
|
| 35 |
+
<ul className="flex space-x-6">
|
| 36 |
+
{navigation.map((item) => (
|
| 37 |
+
<li key={item.path}>
|
| 38 |
+
<Link
|
| 39 |
+
to={item.path}
|
| 40 |
+
className={`hover:text-green-200 transition font-medium drop-shadow ${
|
| 41 |
+
location.pathname === item.path
|
| 42 |
+
? 'font-bold underline underline-offset-4 decoration-2 bg-gradient-to-r from-yellow-300 to-green-300 bg-clip-text text-transparent'
|
| 43 |
+
: 'bg-gradient-to-r from-white to-gray-200 bg-clip-text text-transparent dark:from-gray-100 dark:to-gray-300'
|
| 44 |
+
}`}
|
| 45 |
+
>
|
| 46 |
+
{item.name}
|
| 47 |
+
</Link>
|
| 48 |
+
</li>
|
| 49 |
+
))}
|
| 50 |
+
</ul>
|
| 51 |
+
</nav>
|
| 52 |
+
<button
|
| 53 |
+
onClick={toggleTheme}
|
| 54 |
+
className="p-2 rounded-full bg-white/20 hover:bg-white/30 transition drop-shadow"
|
| 55 |
+
aria-label="Toggle dark mode"
|
| 56 |
+
>
|
| 57 |
+
{theme === 'light' ? (
|
| 58 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 drop-shadow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 59 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
| 60 |
+
</svg>
|
| 61 |
+
) : (
|
| 62 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 drop-shadow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 63 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
| 64 |
+
</svg>
|
| 65 |
+
)}
|
| 66 |
+
</button>
|
| 67 |
+
<button className="md:hidden text-white">
|
| 68 |
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 drop-shadow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 69 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
| 70 |
+
</svg>
|
| 71 |
+
</button>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</header>
|
| 75 |
+
|
| 76 |
+
<main>
|
| 77 |
+
{children}
|
| 78 |
+
</main>
|
| 79 |
+
|
| 80 |
+
<footer className="bg-gradient-to-r from-gray-800 via-gray-900 to-gray-800 text-white py-12 dark:from-gray-900 dark:via-black dark:to-gray-900 transition-colors duration-300">
|
| 81 |
+
<div className="container mx-auto px-4">
|
| 82 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
| 83 |
+
<div>
|
| 84 |
+
<h3 className="text-xl font-bold mb-4 drop-shadow">
|
| 85 |
+
<span className="bg-gradient-to-r from-green-300 to-blue-300 bg-clip-text text-transparent">
|
| 86 |
+
GXS BioNexus Hub
|
| 87 |
+
</span>
|
| 88 |
+
</h3>
|
| 89 |
+
<p className="text-gray-400 drop-shadow">Accelerating bioeconomy solutions that reduce food loss & waste.</p>
|
| 90 |
+
</div>
|
| 91 |
+
<div>
|
| 92 |
+
<h4 className="font-bold mb-4 drop-shadow">
|
| 93 |
+
<span className="bg-gradient-to-r from-green-300 to-blue-300 bg-clip-text text-transparent">
|
| 94 |
+
Platform
|
| 95 |
+
</span>
|
| 96 |
+
</h4>
|
| 97 |
+
<ul className="space-y-2 text-gray-400">
|
| 98 |
+
<li><Link to="/" className="hover:text-white transition drop-shadow">Home</Link></li>
|
| 99 |
+
<li><Link to="/news" className="hover:text-white transition drop-shadow">News Hub</Link></li>
|
| 100 |
+
<li><Link to="/datasets" className="hover:text-white transition drop-shadow">Datasets</Link></li>
|
| 101 |
+
<li><Link to="/directory" className="hover:text-white transition drop-shadow">Directory</Link></li>
|
| 102 |
+
<li><Link to="/matchmaking" className="hover:text-white transition drop-shadow">Matchmaking</Link></li>
|
| 103 |
+
</ul>
|
| 104 |
+
</div>
|
| 105 |
+
<div>
|
| 106 |
+
<h4 className="font-bold mb-4 drop-shadow">
|
| 107 |
+
<span className="bg-gradient-to-r from-green-300 to-blue-300 bg-clip-text text-transparent">
|
| 108 |
+
Resources
|
| 109 |
+
</span>
|
| 110 |
+
</h4>
|
| 111 |
+
<ul className="space-y-2 text-gray-400">
|
| 112 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">Documentation</a></li>
|
| 113 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">API</a></li>
|
| 114 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">Community</a></li>
|
| 115 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">Blog</a></li>
|
| 116 |
+
</ul>
|
| 117 |
+
</div>
|
| 118 |
+
<div>
|
| 119 |
+
<h4 className="font-bold mb-4 drop-shadow">
|
| 120 |
+
<span className="bg-gradient-to-r from-green-300 to-blue-300 bg-clip-text text-transparent">
|
| 121 |
+
Connect
|
| 122 |
+
</span>
|
| 123 |
+
</h4>
|
| 124 |
+
<ul className="space-y-2 text-gray-400">
|
| 125 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">Twitter</a></li>
|
| 126 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">LinkedIn</a></li>
|
| 127 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">GitHub</a></li>
|
| 128 |
+
<li><a href="#" className="hover:text-white transition drop-shadow">Contact</a></li>
|
| 129 |
+
</ul>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
<div className="border-t border-gray-700 mt-8 pt-8 text-center text-gray-400 drop-shadow">
|
| 133 |
+
<p>© 2025 GXS BioNexus Hub. All rights reserved.</p>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</footer>
|
| 137 |
+
</div>
|
| 138 |
+
);
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
export default MainLayout;
|
src/main.jsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
| 4 |
+
import MainLayout from './layouts/MainLayout'
|
| 5 |
+
import HomePage from './pages/HomePage'
|
| 6 |
+
import NewsPage from './pages/NewsPage'
|
| 7 |
+
import NewsDetailPage from './pages/NewsDetailPage'
|
| 8 |
+
import DatasetsPage from './pages/DatasetsPage'
|
| 9 |
+
import DirectoryPage from './pages/DirectoryPage'
|
| 10 |
+
import MatchmakingPage from './pages/MatchmakingPage'
|
| 11 |
+
import NotFoundPage from './pages/NotFoundPage'
|
| 12 |
+
import './index.css'
|
| 13 |
+
|
| 14 |
+
const router = createBrowserRouter([
|
| 15 |
+
{
|
| 16 |
+
path: "/",
|
| 17 |
+
element: <MainLayout><HomePage /></MainLayout>
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
path: "/news",
|
| 21 |
+
element: <MainLayout><NewsPage /></MainLayout>
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
path: "/news/:id",
|
| 25 |
+
element: <MainLayout><NewsDetailPage /></MainLayout>
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
path: "/datasets",
|
| 29 |
+
element: <MainLayout><DatasetsPage /></MainLayout>
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
path: "/directory",
|
| 33 |
+
element: <MainLayout><DirectoryPage /></MainLayout>
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
path: "/matchmaking",
|
| 37 |
+
element: <MainLayout><MatchmakingPage /></MainLayout>
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
path: "*",
|
| 41 |
+
element: <MainLayout><NotFoundPage /></MainLayout>
|
| 42 |
+
}
|
| 43 |
+
])
|
| 44 |
+
|
| 45 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 46 |
+
<React.StrictMode>
|
| 47 |
+
<RouterProvider router={router} />
|
| 48 |
+
</React.StrictMode>,
|
| 49 |
+
)
|
src/pages/DatasetsPage.jsx
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const DatasetsPage = () => {
|
| 5 |
+
const [datasets, setDatasets] = useState([]);
|
| 6 |
+
const [loading, setLoading] = useState(true);
|
| 7 |
+
const [error, setError] = useState(null);
|
| 8 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 9 |
+
const [categoryFilter, setCategoryFilter] = useState('All');
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
// In a real app, this would fetch from an API
|
| 13 |
+
// For demo purposes, we'll use mock data
|
| 14 |
+
const mockDatasets = [
|
| 15 |
+
{
|
| 16 |
+
id: 1,
|
| 17 |
+
title: 'Cold Chain Infrastructure Map',
|
| 18 |
+
description: 'Geospatial data on refrigeration facilities across Sub-Saharan Africa. Includes location, capacity, and operational status of cold storage facilities.',
|
| 19 |
+
category: 'Infrastructure',
|
| 20 |
+
size: '2.4 GB',
|
| 21 |
+
downloads: 1240,
|
| 22 |
+
tags: ['geospatial', 'refrigeration', 'Africa', 'logistics']
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
id: 2,
|
| 26 |
+
title: 'Post-Harvest Loss Hotspots',
|
| 27 |
+
description: 'Identified areas with highest food loss rates in South Asia. Contains data on crop types, loss percentages, and contributing factors.',
|
| 28 |
+
category: 'Analytics',
|
| 29 |
+
size: '890 MB',
|
| 30 |
+
downloads: 890,
|
| 31 |
+
tags: ['analytics', 'loss assessment', 'South Asia', 'crops']
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
id: 3,
|
| 35 |
+
title: 'Processing Capacity Database',
|
| 36 |
+
description: 'List of food processing facilities with available capacity. Includes contact information, equipment types, and scheduling availability.',
|
| 37 |
+
category: 'Industry',
|
| 38 |
+
size: '1.1 GB',
|
| 39 |
+
downloads: 1560,
|
| 40 |
+
tags: ['processing', 'manufacturing', 'capacity', 'directory']
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
id: 4,
|
| 44 |
+
title: 'Food Waste Composition Analysis',
|
| 45 |
+
description: 'Detailed analysis of food waste streams in urban environments. Data covers waste composition by type, seasonal variations, and disposal methods.',
|
| 46 |
+
category: 'Waste Management',
|
| 47 |
+
size: '3.2 GB',
|
| 48 |
+
downloads: 2100,
|
| 49 |
+
tags: ['waste', 'composition', 'urban', 'analysis']
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
id: 5,
|
| 53 |
+
title: 'Agricultural Surplus Mapping',
|
| 54 |
+
description: 'Real-time mapping of agricultural surpluses available for redistribution. Updated weekly with data from farms and cooperatives.',
|
| 55 |
+
category: 'Supply Chain',
|
| 56 |
+
size: '1.7 GB',
|
| 57 |
+
downloads: 980,
|
| 58 |
+
tags: ['surplus', 'redistribution', 'mapping', 'real-time']
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
id: 6,
|
| 62 |
+
title: 'Policy Instruments Database',
|
| 63 |
+
description: 'Comprehensive database of food loss and waste reduction policies worldwide. Includes policy descriptions, implementation status, and impact assessments.',
|
| 64 |
+
category: 'Policy',
|
| 65 |
+
size: '520 MB',
|
| 66 |
+
downloads: 1650,
|
| 67 |
+
tags: ['policy', 'regulations', 'government', 'database']
|
| 68 |
+
}
|
| 69 |
+
];
|
| 70 |
+
|
| 71 |
+
setTimeout(() => {
|
| 72 |
+
setDatasets(mockDatasets);
|
| 73 |
+
setLoading(false);
|
| 74 |
+
}, 500);
|
| 75 |
+
}, []);
|
| 76 |
+
|
| 77 |
+
const categories = ['All', ...new Set(datasets.map(dataset => dataset.category))];
|
| 78 |
+
|
| 79 |
+
const filteredDatasets = datasets.filter(dataset => {
|
| 80 |
+
const matchesSearch = dataset.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 81 |
+
dataset.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 82 |
+
dataset.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
|
| 83 |
+
|
| 84 |
+
const matchesCategory = categoryFilter === 'All' || dataset.category === categoryFilter;
|
| 85 |
+
|
| 86 |
+
return matchesSearch && matchesCategory;
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
if (loading) {
|
| 90 |
+
return (
|
| 91 |
+
<div className="py-16 bg-gradient-to-br from-white to-gray-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300 min-h-screen">
|
| 92 |
+
<div className="container mx-auto px-4">
|
| 93 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Open Datasets</h2>
|
| 94 |
+
<div className="flex justify-center">
|
| 95 |
+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-bio-green"></div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if (error) {
|
| 103 |
+
return (
|
| 104 |
+
<div className="py-16 bg-gradient-to-br from-white to-gray-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300 min-h-screen">
|
| 105 |
+
<div className="container mx-auto px-4">
|
| 106 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Open Datasets</h2>
|
| 107 |
+
<div className="text-center text-red-500">
|
| 108 |
+
<p>Error loading datasets: {error}</p>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
return (
|
| 116 |
+
<div className="py-16 bg-gradient-to-br from-white to-gray-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300 min-h-screen">
|
| 117 |
+
<div className="container mx-auto px-4">
|
| 118 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Open Datasets</h2>
|
| 119 |
+
|
| 120 |
+
<div className="mb-8 bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 border border-gray-100 dark:border-gray-700 bg-gradient-to-br from-white to-gray-100 dark:from-gray-800 dark:to-gray-900">
|
| 121 |
+
<div className="flex flex-col md:flex-row gap-4">
|
| 122 |
+
<div className="flex-grow">
|
| 123 |
+
<input
|
| 124 |
+
type="text"
|
| 125 |
+
placeholder="Search datasets..."
|
| 126 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-700 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-700 dark:to-gray-800"
|
| 127 |
+
value={searchTerm}
|
| 128 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 129 |
+
/>
|
| 130 |
+
</div>
|
| 131 |
+
<div className="w-full md:w-48">
|
| 132 |
+
<select
|
| 133 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-700 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-700 dark:to-gray-800"
|
| 134 |
+
value={categoryFilter}
|
| 135 |
+
onChange={(e) => setCategoryFilter(e.target.value)}
|
| 136 |
+
>
|
| 137 |
+
{categories.map(category => (
|
| 138 |
+
<option key={category} value={category}>{category}</option>
|
| 139 |
+
))}
|
| 140 |
+
</select>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
| 146 |
+
{filteredDatasets.map((dataset) => (
|
| 147 |
+
<div key={dataset.id} className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg p-6 border border-blue-100 dark:border-gray-600 transition-all duration-300 transform hover:-translate-y-1 hover:shadow-xl bg-gradient-to-br from-blue-50/50 to-purple-50/50 dark:from-gray-700/50 dark:to-gray-800/50">
|
| 148 |
+
<div className="flex justify-between items-start mb-4">
|
| 149 |
+
<h3 className="text-xl font-bold text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">{dataset.title}</h3>
|
| 150 |
+
<span className="inline-block px-3 py-1 text-xs font-semibold text-bio-blue bg-blue-100 dark:bg-blue-900/30 dark:text-blue-300 rounded-full drop-shadow bg-gradient-to-r from-blue-100 to-blue-200 dark:from-blue-900/30 dark:to-blue-800/30">
|
| 151 |
+
{dataset.category}
|
| 152 |
+
</span>
|
| 153 |
+
</div>
|
| 154 |
+
<p className="text-gray-600 dark:text-gray-300 mb-4 drop-shadow">{dataset.description}</p>
|
| 155 |
+
|
| 156 |
+
<div className="flex flex-wrap gap-2 mb-4">
|
| 157 |
+
{dataset.tags.map((tag, index) => (
|
| 158 |
+
<span key={index} className="px-2 py-1 text-xs bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-full drop-shadow bg-gradient-to-r from-gray-200 to-gray-300 dark:from-gray-600 dark:to-gray-700">
|
| 159 |
+
{tag}
|
| 160 |
+
</span>
|
| 161 |
+
))}
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<div className="flex justify-between items-center">
|
| 165 |
+
<div className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">
|
| 166 |
+
<span className="mr-4">Size: {dataset.size}</span>
|
| 167 |
+
<span>{dataset.downloads} downloads</span>
|
| 168 |
+
</div>
|
| 169 |
+
<div className="flex gap-2">
|
| 170 |
+
<button className="bg-gradient-to-r from-bio-blue to-blue-500 text-white px-4 py-2 rounded-lg hover:from-blue-500 hover:to-blue-600 transition shadow-md hover:shadow-lg drop-shadow">
|
| 171 |
+
Download
|
| 172 |
+
</button>
|
| 173 |
+
<button className="border border-bio-blue text-bio-blue dark:text-blue-400 px-4 py-2 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900/30 transition drop-shadow bg-gradient-to-r from-white to-blue-50 dark:from-gray-800 dark:to-blue-900/30">
|
| 174 |
+
Preview
|
| 175 |
+
</button>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
))}
|
| 180 |
+
</div>
|
| 181 |
+
|
| 182 |
+
{filteredDatasets.length === 0 && (
|
| 183 |
+
<div className="text-center py-12 bg-white dark:bg-gray-800 rounded-2xl shadow-lg bg-gradient-to-br from-white to-gray-100 dark:from-gray-800 dark:to-gray-900">
|
| 184 |
+
<p className="text-gray-600 dark:text-gray-400 drop-shadow">No datasets found matching your criteria.</p>
|
| 185 |
+
<button
|
| 186 |
+
className="mt-4 text-bio-blue dark:text-blue-400 hover:underline drop-shadow bg-gradient-to-r from-bio-blue to-blue-500 bg-clip-text text-transparent"
|
| 187 |
+
onClick={() => {
|
| 188 |
+
setSearchTerm('');
|
| 189 |
+
setCategoryFilter('All');
|
| 190 |
+
}}
|
| 191 |
+
>
|
| 192 |
+
Clear filters
|
| 193 |
+
</button>
|
| 194 |
+
</div>
|
| 195 |
+
)}
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
);
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
+
export default DatasetsPage;
|
src/pages/DirectoryPage.jsx
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
|
| 3 |
+
const DirectoryPage = () => {
|
| 4 |
+
const [organizations, setOrganizations] = useState([]);
|
| 5 |
+
const [loading, setLoading] = useState(true);
|
| 6 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 7 |
+
const [sectorFilter, setSectorFilter] = useState('All');
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
// In a real app, this would fetch from an API
|
| 11 |
+
// For demo purposes, we'll use mock data
|
| 12 |
+
const mockOrganizations = [
|
| 13 |
+
{
|
| 14 |
+
id: 1,
|
| 15 |
+
name: 'Global Food Rescue Initiative',
|
| 16 |
+
sector: 'NGO',
|
| 17 |
+
country: 'International',
|
| 18 |
+
description: 'Non-profit organization focused on reducing food waste through redistribution networks.',
|
| 19 |
+
expertise: ['Food Recovery', 'Logistics', 'Volunteer Management'],
|
| 20 |
+
projects: 42
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
id: 2,
|
| 24 |
+
name: 'AgriTech Solutions Ltd',
|
| 25 |
+
sector: 'Private',
|
| 26 |
+
country: 'Netherlands',
|
| 27 |
+
description: 'Technology company developing smart cold chain solutions for perishable goods.',
|
| 28 |
+
expertise: ['Cold Chain Technology', 'IoT Sensors', 'Data Analytics'],
|
| 29 |
+
projects: 18
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
id: 3,
|
| 33 |
+
name: 'Sustainable Agriculture Research Institute',
|
| 34 |
+
sector: 'Academic',
|
| 35 |
+
country: 'United States',
|
| 36 |
+
description: 'Research institution studying post-harvest loss reduction techniques.',
|
| 37 |
+
expertise: ['Research', 'Post-harvest Technology', 'Training'],
|
| 38 |
+
projects: 27
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
id: 4,
|
| 42 |
+
name: 'Ministry of Agriculture and Food Security',
|
| 43 |
+
sector: 'Government',
|
| 44 |
+
country: 'Kenya',
|
| 45 |
+
description: 'Government agency implementing national food loss reduction programs.',
|
| 46 |
+
expertise: ['Policy Development', 'Program Implementation', 'Farmer Support'],
|
| 47 |
+
projects: 15
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
id: 5,
|
| 51 |
+
name: 'Circular Economy Food Partners',
|
| 52 |
+
sector: 'NGO',
|
| 53 |
+
country: 'Germany',
|
| 54 |
+
description: 'Organization promoting circular economy approaches to food systems.',
|
| 55 |
+
expertise: ['Circular Economy', 'Composting', 'Waste Valorization'],
|
| 56 |
+
projects: 33
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
id: 6,
|
| 60 |
+
name: 'Farm to Market Logistics Co.',
|
| 61 |
+
sector: 'Private',
|
| 62 |
+
country: 'Brazil',
|
| 63 |
+
description: 'Logistics company specializing in transportation of perishable agricultural products.',
|
| 64 |
+
expertise: ['Transportation', 'Cold Chain Logistics', 'Supply Chain'],
|
| 65 |
+
projects: 29
|
| 66 |
+
}
|
| 67 |
+
];
|
| 68 |
+
|
| 69 |
+
setTimeout(() => {
|
| 70 |
+
setOrganizations(mockOrganizations);
|
| 71 |
+
setLoading(false);
|
| 72 |
+
}, 500);
|
| 73 |
+
}, []);
|
| 74 |
+
|
| 75 |
+
const sectors = ['All', ...new Set(organizations.map(org => org.sector))];
|
| 76 |
+
|
| 77 |
+
const filteredOrganizations = organizations.filter(org => {
|
| 78 |
+
const matchesSearch = org.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 79 |
+
org.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 80 |
+
org.expertise.some(exp => exp.toLowerCase().includes(searchTerm.toLowerCase()));
|
| 81 |
+
|
| 82 |
+
const matchesSector = sectorFilter === 'All' || org.sector === sectorFilter;
|
| 83 |
+
|
| 84 |
+
return matchesSearch && matchesSector;
|
| 85 |
+
});
|
| 86 |
+
|
| 87 |
+
if (loading) {
|
| 88 |
+
return (
|
| 89 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 90 |
+
<div className="container mx-auto px-4">
|
| 91 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Stakeholder Directory</h2>
|
| 92 |
+
<div className="flex justify-center">
|
| 93 |
+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-bio-green"></div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
return (
|
| 101 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 102 |
+
<div className="container mx-auto px-4">
|
| 103 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Stakeholder Directory</h2>
|
| 104 |
+
|
| 105 |
+
<div className="mb-8 bg-white dark:bg-gray-700 rounded-2xl shadow-lg p-6 border border-gray-100 dark:border-gray-600 bg-gradient-to-br from-white to-gray-100 dark:from-gray-700 dark:to-gray-800">
|
| 106 |
+
<div className="flex flex-col md:flex-row gap-4">
|
| 107 |
+
<div className="flex-grow">
|
| 108 |
+
<input
|
| 109 |
+
type="text"
|
| 110 |
+
placeholder="Search organizations..."
|
| 111 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-600 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-600 dark:to-gray-700"
|
| 112 |
+
value={searchTerm}
|
| 113 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 114 |
+
/>
|
| 115 |
+
</div>
|
| 116 |
+
<div className="w-full md:w-48">
|
| 117 |
+
<select
|
| 118 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-600 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-600 dark:to-gray-700"
|
| 119 |
+
value={sectorFilter}
|
| 120 |
+
onChange={(e) => setSectorFilter(e.target.value)}
|
| 121 |
+
>
|
| 122 |
+
{sectors.map(sector => (
|
| 123 |
+
<option key={sector} value={sector}>{sector}</option>
|
| 124 |
+
))}
|
| 125 |
+
</select>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 131 |
+
{filteredOrganizations.map((org) => (
|
| 132 |
+
<div key={org.id} className="bg-white dark:bg-gray-700 rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 border border-gray-100 dark:border-gray-600 bg-gradient-to-br from-white to-gray-100 dark:from-gray-700 dark:to-gray-800">
|
| 133 |
+
<div className="p-6">
|
| 134 |
+
<div className="flex justify-between items-start mb-4">
|
| 135 |
+
<h3 className="text-xl font-bold text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">{org.name}</h3>
|
| 136 |
+
<span className="inline-block px-3 py-1 text-xs font-semibold text-bio-purple bg-purple-100 dark:bg-purple-900/30 dark:text-purple-300 rounded-full drop-shadow bg-gradient-to-r from-purple-100 to-purple-200 dark:from-purple-900/30 dark:to-purple-800/30">
|
| 137 |
+
{org.sector}
|
| 138 |
+
</span>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<div className="mb-4">
|
| 142 |
+
<span className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">{org.country}</span>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<p className="text-gray-600 dark:text-gray-300 mb-4 drop-shadow">{org.description}</p>
|
| 146 |
+
|
| 147 |
+
<div className="mb-4">
|
| 148 |
+
<h4 className="font-semibold text-gray-800 dark:text-white mb-2 drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">Expertise:</h4>
|
| 149 |
+
<div className="flex flex-wrap gap-2">
|
| 150 |
+
{org.expertise.map((exp, index) => (
|
| 151 |
+
<span key={index} className="px-2 py-1 text-xs bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-full drop-shadow bg-gradient-to-r from-gray-200 to-gray-300 dark:from-gray-600 dark:to-gray-700">
|
| 152 |
+
{exp}
|
| 153 |
+
</span>
|
| 154 |
+
))}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div className="flex justify-between items-center">
|
| 159 |
+
<span className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">{org.projects} projects</span>
|
| 160 |
+
<button className="bg-gradient-to-r from-bio-purple to-purple-500 text-white px-4 py-2 rounded-lg hover:from-purple-500 hover:to-purple-600 transition shadow-md hover:shadow-lg drop-shadow">
|
| 161 |
+
Contact
|
| 162 |
+
</button>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
))}
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
{filteredOrganizations.length === 0 && (
|
| 170 |
+
<div className="text-center py-12 bg-white dark:bg-gray-700 rounded-2xl shadow-lg bg-gradient-to-br from-white to-gray-100 dark:from-gray-700 dark:to-gray-800">
|
| 171 |
+
<p className="text-gray-600 dark:text-gray-400 drop-shadow">No organizations found matching your criteria.</p>
|
| 172 |
+
<button
|
| 173 |
+
className="mt-4 text-bio-purple dark:text-purple-400 hover:underline drop-shadow bg-gradient-to-r from-bio-purple to-purple-500 bg-clip-text text-transparent"
|
| 174 |
+
onClick={() => {
|
| 175 |
+
setSearchTerm('');
|
| 176 |
+
setSectorFilter('All');
|
| 177 |
+
}}
|
| 178 |
+
>
|
| 179 |
+
Clear filters
|
| 180 |
+
</button>
|
| 181 |
+
</div>
|
| 182 |
+
)}
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
);
|
| 186 |
+
};
|
| 187 |
+
|
| 188 |
+
export default DirectoryPage;
|
src/pages/HomePage.jsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const HomePage = () => {
|
| 5 |
+
const features = [
|
| 6 |
+
{
|
| 7 |
+
title: "News Hub",
|
| 8 |
+
description: "Discover regional policy updates and best practices in food loss reduction and bioeconomy innovation.",
|
| 9 |
+
icon: "📰",
|
| 10 |
+
link: "/news",
|
| 11 |
+
gradient: "from-bio-green to-green-500"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
title: "Open Data",
|
| 15 |
+
description: "Access datasets on cold-chain gaps, processing capacity, and surplus hotspots to inform interventions.",
|
| 16 |
+
icon: "📊",
|
| 17 |
+
link: "/datasets",
|
| 18 |
+
gradient: "from-bio-blue to-blue-500"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
title: "Matchmaking",
|
| 22 |
+
description: "Connect with partners, funders, and implementers to develop collaborative pilot projects.",
|
| 23 |
+
icon: "🤝",
|
| 24 |
+
link: "/matchmaking",
|
| 25 |
+
gradient: "from-bio-purple to-purple-500"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
title: "Directory",
|
| 29 |
+
description: "Find and connect with stakeholders across the bioeconomy ecosystem.",
|
| 30 |
+
icon: "📖",
|
| 31 |
+
link: "/directory",
|
| 32 |
+
gradient: "from-bio-orange to-orange-500"
|
| 33 |
+
}
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<div className="bg-gradient-to-br from-green-50 via-blue-50 to-purple-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
|
| 38 |
+
{/* Enhanced hero section with more vibrant gradient */}
|
| 39 |
+
<section className="hero py-16 md:py-24 bg-gradient-to-r from-bio-green via-bio-blue to-bio-purple text-white dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 transition-all duration-300">
|
| 40 |
+
<div className="container mx-auto px-4 text-center">
|
| 41 |
+
<h2 className="text-4xl md:text-6xl font-bold mb-6 drop-shadow-lg [-webkit-text-stroke: 0.5px_rgba(0,0,0,0.1)] bg-gradient-to-r from-yellow-300 via-green-300 to-blue-300 bg-clip-text text-transparent">
|
| 42 |
+
Connecting Stakeholders to Accelerate Bioeconomy Solutions
|
| 43 |
+
</h2>
|
| 44 |
+
<p className="text-xl md:text-2xl max-w-3xl mx-auto mb-10 drop-shadow-md font-medium bg-gradient-to-r from-green-200 via-blue-200 to-purple-200 bg-clip-text text-transparent">
|
| 45 |
+
Reducing Food Loss & Waste through data, partnerships, and pilot projects
|
| 46 |
+
</p>
|
| 47 |
+
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
| 48 |
+
<Link
|
| 49 |
+
to="/datasets"
|
| 50 |
+
className="bg-gradient-to-r from-bio-green to-green-500 text-white font-bold py-3 px-8 rounded-full hover:from-green-500 hover:to-green-600 transition-all transform hover:scale-105 shadow-lg text-center shadow-green-500/30 hover:shadow-green-500/50"
|
| 51 |
+
>
|
| 52 |
+
Explore Datasets
|
| 53 |
+
</Link>
|
| 54 |
+
<Link
|
| 55 |
+
to="/matchmaking"
|
| 56 |
+
className="bg-gradient-to-r from-bio-purple to-purple-500 text-white font-bold py-3 px-8 rounded-full hover:from-purple-500 hover:to-purple-600 transition-all transform hover:scale-105 shadow-lg text-center shadow-purple-500/30 hover:shadow-purple-500/50"
|
| 57 |
+
>
|
| 58 |
+
Join Matchmaking
|
| 59 |
+
</Link>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
</section>
|
| 63 |
+
|
| 64 |
+
{/* Enhanced features section with colorful gradients */}
|
| 65 |
+
<section className="py-16 bg-gradient-to-br from-white to-gray-100 dark:from-gray-800 dark:to-gray-900">
|
| 66 |
+
<div className="container mx-auto px-4">
|
| 67 |
+
<h2 className="text-3xl font-bold text-center mb-12 text-gray-800 dark:text-white bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">
|
| 68 |
+
Our Platform Features
|
| 69 |
+
</h2>
|
| 70 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
| 71 |
+
{features.map((feature, index) => (
|
| 72 |
+
<Link
|
| 73 |
+
to={feature.link}
|
| 74 |
+
key={index}
|
| 75 |
+
className={`bg-gradient-to-br ${feature.gradient} dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl shadow-xl border border-white/20 dark:border-gray-700 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 block text-white hover:from-bio-pink hover:to-bio-teal`}
|
| 76 |
+
>
|
| 77 |
+
<div className="text-5xl mb-4 drop-shadow-md">{feature.icon}</div>
|
| 78 |
+
<h3 className="text-2xl font-bold mb-3 drop-shadow bg-gradient-to-r from-yellow-300 to-white bg-clip-text text-transparent">{feature.title}</h3>
|
| 79 |
+
<p className="drop-shadow">{feature.description}</p>
|
| 80 |
+
</Link>
|
| 81 |
+
))}
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
</section>
|
| 85 |
+
|
| 86 |
+
{/* Enhanced CTA section with vibrant gradient */}
|
| 87 |
+
<section className="py-16 bg-gradient-to-r from-bio-teal via-bio-blue to-bio-purple dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 transition-colors duration-300">
|
| 88 |
+
<div className="container mx-auto px-4 text-center">
|
| 89 |
+
<h2 className="text-3xl font-bold mb-6 text-white drop-shadow-lg bg-gradient-to-r from-yellow-300 to-white bg-clip-text text-transparent">
|
| 90 |
+
Join Our Growing Network
|
| 91 |
+
</h2>
|
| 92 |
+
<p className="text-xl max-w-3xl mx-auto mb-10 text-white/90 drop-shadow-md font-medium bg-gradient-to-r from-green-200 to-blue-200 bg-clip-text text-transparent">
|
| 93 |
+
Be part of a global community working to transform agrifood systems through bioeconomy solutions.
|
| 94 |
+
</p>
|
| 95 |
+
<Link
|
| 96 |
+
to="/directory"
|
| 97 |
+
className="bg-gradient-to-r from-bio-pink to-pink-500 text-white font-bold py-3 px-8 rounded-full hover:from-pink-500 hover:to-pink-600 transition-all transform hover:scale-105 shadow-lg inline-block shadow-pink-500/30 hover:shadow-pink-500/50"
|
| 98 |
+
>
|
| 99 |
+
Explore Directory
|
| 100 |
+
</Link>
|
| 101 |
+
</div>
|
| 102 |
+
</section>
|
| 103 |
+
</div>
|
| 104 |
+
);
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
export default HomePage;
|
src/pages/MatchmakingPage.jsx
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
|
| 3 |
+
const MatchmakingPage = () => {
|
| 4 |
+
const [activeTab, setActiveTab] = useState('opportunities');
|
| 5 |
+
const [formData, setFormData] = useState({
|
| 6 |
+
name: '',
|
| 7 |
+
email: '',
|
| 8 |
+
organization: '',
|
| 9 |
+
interest: '',
|
| 10 |
+
description: ''
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
const opportunities = [
|
| 14 |
+
{
|
| 15 |
+
id: 1,
|
| 16 |
+
title: 'Solar-Powered Cold Storage for Smallholder Farmers',
|
| 17 |
+
organization: 'Renewable Agriculture Initiative',
|
| 18 |
+
location: 'Kenya, Tanzania',
|
| 19 |
+
description: 'Seeking technology partners and investors for deployment of solar-powered cold storage units in East Africa.',
|
| 20 |
+
tags: ['Cold Chain', 'Solar Power', 'Smallholders', 'East Africa'],
|
| 21 |
+
posted: '2025-10-18'
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
id: 2,
|
| 25 |
+
title: 'Mobile Processing Unit for Fruit Preservation',
|
| 26 |
+
organization: 'Sustainable Food Systems Group',
|
| 27 |
+
location: 'India, Bangladesh',
|
| 28 |
+
description: 'Looking for equipment manufacturers and local partners to deploy mobile processing units for mango and banana preservation.',
|
| 29 |
+
tags: ['Processing', 'Mobile Units', 'Fruit Preservation', 'South Asia'],
|
| 30 |
+
posted: '2025-10-15'
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
id: 3,
|
| 34 |
+
title: 'Food Waste to Biogas Project',
|
| 35 |
+
organization: 'Circular Economy Network',
|
| 36 |
+
location: 'Brazil, Colombia',
|
| 37 |
+
description: 'Seeking technology providers and financing partners for food waste-to-biogas facilities in urban areas.',
|
| 38 |
+
tags: ['Biogas', 'Waste Valorization', 'Urban', 'South America'],
|
| 39 |
+
posted: '2025-10-12'
|
| 40 |
+
}
|
| 41 |
+
];
|
| 42 |
+
|
| 43 |
+
const handleSubmit = (e) => {
|
| 44 |
+
e.preventDefault();
|
| 45 |
+
// In a real app, this would submit to an API
|
| 46 |
+
alert('Thank you for your submission! We will review your request and get back to you soon.');
|
| 47 |
+
setFormData({
|
| 48 |
+
name: '',
|
| 49 |
+
email: '',
|
| 50 |
+
organization: '',
|
| 51 |
+
interest: '',
|
| 52 |
+
description: ''
|
| 53 |
+
});
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
const handleChange = (e) => {
|
| 57 |
+
setFormData({
|
| 58 |
+
...formData,
|
| 59 |
+
[e.target.name]: e.target.value
|
| 60 |
+
});
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
return (
|
| 64 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 65 |
+
<div className="container mx-auto px-4">
|
| 66 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">Matchmaking Platform</h2>
|
| 67 |
+
|
| 68 |
+
<div className="bg-white dark:bg-gray-700 rounded-2xl shadow-lg overflow-hidden mb-8 border border-gray-100 dark:border-gray-600 bg-gradient-to-br from-white to-gray-100 dark:from-gray-700 dark:to-gray-800">
|
| 69 |
+
<div className="border-b border-gray-200 dark:border-gray-600">
|
| 70 |
+
<nav className="flex">
|
| 71 |
+
<button
|
| 72 |
+
className={`py-4 px-6 text-center border-b-2 font-medium text-sm ${
|
| 73 |
+
activeTab === 'opportunities'
|
| 74 |
+
? 'border-bio-green text-bio-green dark:text-green-400 drop-shadow bg-gradient-to-r from-bio-green to-green-500 bg-clip-text text-transparent'
|
| 75 |
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
| 76 |
+
}`}
|
| 77 |
+
onClick={() => setActiveTab('opportunities')}
|
| 78 |
+
>
|
| 79 |
+
Collaboration Opportunities
|
| 80 |
+
</button>
|
| 81 |
+
<button
|
| 82 |
+
className={`py-4 px-6 text-center border-b-2 font-medium text-sm ${
|
| 83 |
+
activeTab === 'submit'
|
| 84 |
+
? 'border-bio-green text-bio-green dark:text-green-400 drop-shadow bg-gradient-to-r from-bio-green to-green-500 bg-clip-text text-transparent'
|
| 85 |
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
| 86 |
+
}`}
|
| 87 |
+
onClick={() => setActiveTab('submit')}
|
| 88 |
+
>
|
| 89 |
+
Submit Opportunity
|
| 90 |
+
</button>
|
| 91 |
+
</nav>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div className="p-6">
|
| 95 |
+
{activeTab === 'opportunities' ? (
|
| 96 |
+
<div>
|
| 97 |
+
<h3 className="text-xl font-bold mb-6 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">Current Collaboration Opportunities</h3>
|
| 98 |
+
|
| 99 |
+
<div className="space-y-6">
|
| 100 |
+
{opportunities.map((opportunity) => (
|
| 101 |
+
<div key={opportunity.id} className="border border-gray-200 dark:border-gray-600 rounded-xl p-6 hover:shadow-md transition drop-shadow bg-gradient-to-br from-white to-gray-50 dark:from-gray-700 dark:to-gray-800">
|
| 102 |
+
<div className="flex justify-between items-start mb-4">
|
| 103 |
+
<h4 className="text-lg font-bold text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">{opportunity.title}</h4>
|
| 104 |
+
<span className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">{opportunity.posted}</span>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<div className="mb-3">
|
| 108 |
+
<span className="font-medium text-gray-700 dark:text-gray-300 drop-shadow">Organization:</span> {opportunity.organization}
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div className="mb-3">
|
| 112 |
+
<span className="font-medium text-gray-700 dark:text-gray-300 drop-shadow">Location:</span> {opportunity.location}
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<p className="text-gray-600 dark:text-gray-300 mb-4 drop-shadow">{opportunity.description}</p>
|
| 116 |
+
|
| 117 |
+
<div className="flex flex-wrap gap-2 mb-4">
|
| 118 |
+
{opportunity.tags.map((tag, index) => (
|
| 119 |
+
<span key={index} className="px-2 py-1 text-xs bg-bio-blue/10 text-bio-blue dark:bg-blue-900/30 dark:text-blue-300 rounded-full drop-shadow bg-gradient-to-r from-blue-100 to-blue-200 dark:from-blue-900/30 dark:to-blue-800/30">
|
| 120 |
+
{tag}
|
| 121 |
+
</span>
|
| 122 |
+
))}
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<button className="bg-gradient-to-r from-bio-green to-green-500 text-white px-4 py-2 rounded-lg hover:from-green-500 hover:to-green-600 transition shadow-md hover:shadow-lg drop-shadow">
|
| 126 |
+
Express Interest
|
| 127 |
+
</button>
|
| 128 |
+
</div>
|
| 129 |
+
))}
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
) : (
|
| 133 |
+
<div>
|
| 134 |
+
<h3 className="text-xl font-bold mb-6 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">Submit Collaboration Opportunity</h3>
|
| 135 |
+
<p className="text-gray-600 dark:text-gray-300 mb-6 drop-shadow">Have a project or collaboration opportunity? Submit the details below and our team will review it for inclusion in our matchmaking platform.</p>
|
| 136 |
+
|
| 137 |
+
<form onSubmit={handleSubmit} className="space-y-6">
|
| 138 |
+
<div>
|
| 139 |
+
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 drop-shadow bg-gradient-to-r from-gray-700 to-gray-600 bg-clip-text text-transparent dark:from-gray-300 dark:to-gray-400">
|
| 140 |
+
Your Name
|
| 141 |
+
</label>
|
| 142 |
+
<input
|
| 143 |
+
type="text"
|
| 144 |
+
id="name"
|
| 145 |
+
name="name"
|
| 146 |
+
value={formData.name}
|
| 147 |
+
onChange={handleChange}
|
| 148 |
+
required
|
| 149 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-600 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-600 dark:to-gray-700"
|
| 150 |
+
/>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<div>
|
| 154 |
+
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 drop-shadow bg-gradient-to-r from-gray-700 to-gray-600 bg-clip-text text-transparent dark:from-gray-300 dark:to-gray-400">
|
| 155 |
+
Email Address
|
| 156 |
+
</label>
|
| 157 |
+
<input
|
| 158 |
+
type="email"
|
| 159 |
+
id="email"
|
| 160 |
+
name="email"
|
| 161 |
+
value={formData.email}
|
| 162 |
+
onChange={handleChange}
|
| 163 |
+
required
|
| 164 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-600 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-600 dark:to-gray-700"
|
| 165 |
+
/>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<div>
|
| 169 |
+
<label htmlFor="organization" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 drop-shadow bg-gradient-to-r from-gray-700 to-gray-600 bg-clip-text text-transparent dark:from-gray-300 dark:to-gray-400">
|
| 170 |
+
Organization
|
| 171 |
+
</label>
|
| 172 |
+
<input
|
| 173 |
+
type="text"
|
| 174 |
+
id="organization"
|
| 175 |
+
name="organization"
|
| 176 |
+
value={formData.organization}
|
| 177 |
+
onChange={handleChange}
|
| 178 |
+
required
|
| 179 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-600 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-600 dark:to-gray-700"
|
| 180 |
+
/>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<div>
|
| 184 |
+
<label htmlFor="interest" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 drop-shadow bg-gradient-to-r from-gray-700 to-gray-600 bg-clip-text text-transparent dark:from-gray-300 dark:to-gray-400">
|
| 185 |
+
Type of Collaboration
|
| 186 |
+
</label>
|
| 187 |
+
<select
|
| 188 |
+
id="interest"
|
| 189 |
+
name="interest"
|
| 190 |
+
value={formData.interest}
|
| 191 |
+
onChange={handleChange}
|
| 192 |
+
required
|
| 193 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-600 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-600 dark:to-gray-700"
|
| 194 |
+
>
|
| 195 |
+
<option value="">Select an option</option>
|
| 196 |
+
<option value="funding">Funding/Investment</option>
|
| 197 |
+
<option value="technology">Technology Partnership</option>
|
| 198 |
+
<option value="implementation">Project Implementation</option>
|
| 199 |
+
<option value="research">Research Collaboration</option>
|
| 200 |
+
<option value="policy">Policy Development</option>
|
| 201 |
+
<option value="other">Other</option>
|
| 202 |
+
</select>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div>
|
| 206 |
+
<label htmlFor="description" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 drop-shadow bg-gradient-to-r from-gray-700 to-gray-600 bg-clip-text text-transparent dark:from-gray-300 dark:to-gray-400">
|
| 207 |
+
Opportunity Description
|
| 208 |
+
</label>
|
| 209 |
+
<textarea
|
| 210 |
+
id="description"
|
| 211 |
+
name="description"
|
| 212 |
+
value={formData.description}
|
| 213 |
+
onChange={handleChange}
|
| 214 |
+
required
|
| 215 |
+
rows={5}
|
| 216 |
+
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-bio-green focus:border-transparent dark:bg-gray-600 dark:text-white drop-shadow bg-gradient-to-r from-white to-gray-50 dark:from-gray-600 dark:to-gray-700"
|
| 217 |
+
placeholder="Describe your collaboration opportunity in detail..."
|
| 218 |
+
></textarea>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
<div>
|
| 222 |
+
<button
|
| 223 |
+
type="submit"
|
| 224 |
+
className="w-full bg-gradient-to-r from-bio-green to-bio-blue text-white font-bold py-3 px-4 rounded-lg hover:opacity-90 transition shadow-lg drop-shadow"
|
| 225 |
+
>
|
| 226 |
+
Submit Opportunity
|
| 227 |
+
</button>
|
| 228 |
+
</div>
|
| 229 |
+
</form>
|
| 230 |
+
</div>
|
| 231 |
+
)}
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
|
| 235 |
+
<div className="bg-gradient-to-r from-bio-purple to-bio-blue rounded-2xl p-6 text-white shadow-lg">
|
| 236 |
+
<h3 className="text-xl font-bold mb-2 drop-shadow">Need Help Finding Partners?</h3>
|
| 237 |
+
<p className="mb-4 drop-shadow">Our matchmaking team can help connect you with the right partners for your project.</p>
|
| 238 |
+
<button className="bg-white text-bio-purple font-bold px-6 py-2 rounded-lg hover:bg-purple-100 transition shadow-md drop-shadow">
|
| 239 |
+
Contact Matchmaking Team
|
| 240 |
+
</button>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
);
|
| 245 |
+
};
|
| 246 |
+
|
| 247 |
+
export default MatchmakingPage;
|
src/pages/NewsDetailPage.jsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { Link, useParams } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const NewsDetailPage = () => {
|
| 5 |
+
const { id } = useParams();
|
| 6 |
+
const [newsItem, setNewsItem] = useState(null);
|
| 7 |
+
const [loading, setLoading] = useState(true);
|
| 8 |
+
const [error, setError] = useState(null);
|
| 9 |
+
|
| 10 |
+
useEffect(() => {
|
| 11 |
+
// In a real app, this would fetch from an API
|
| 12 |
+
// For demo purposes, we'll use mock data
|
| 13 |
+
const mockNews = [
|
| 14 |
+
{
|
| 15 |
+
id: 1,
|
| 16 |
+
title: 'New Cold Chain Initiative Launched in Southeast Asia',
|
| 17 |
+
excerpt: 'Regional partnership aims to reduce post-harvest losses by 30% through solar-powered refrigeration.',
|
| 18 |
+
date: '2025-10-15',
|
| 19 |
+
category: 'Policy',
|
| 20 |
+
content: 'The Southeast Asian Cold Chain Initiative represents a significant step forward in addressing post-harvest losses in the region. The project, funded by a coalition of international donors, will install solar-powered refrigeration units in rural areas where access to electricity is limited. Early pilot programs have shown promising results, with participating farms reporting a 25% reduction in losses of perishable goods.\n\nThe initiative is being implemented in partnership with local governments, agricultural cooperatives, and technology providers. The first phase will target 500 farms across Thailand, Vietnam, and Indonesia, with plans to expand to additional countries in the region.\n\nAccording to Dr. Maria Santos, the project director, "This initiative addresses a critical gap in our food system. By providing reliable cold storage at the farm level, we can significantly reduce losses while improving farmer incomes and food security."'
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
id: 2,
|
| 24 |
+
title: 'Innovative Edible Coatings Show Promise in Lab Trials',
|
| 25 |
+
excerpt: 'New biodegradable coatings extend shelf life of fruits by up to 2 weeks.',
|
| 26 |
+
date: '2025-10-10',
|
| 27 |
+
category: 'Technology',
|
| 28 |
+
content: 'Scientists at the Institute for Sustainable Agriculture have developed a new type of edible coating derived from plant-based materials that can extend the shelf life of fruits and vegetables. In laboratory trials, apples treated with the coating remained fresh for up to 14 days longer than untreated controls. The coating is completely biodegradable and safe for consumption.\n\nThe coating is made from a blend of polysaccharides and proteins derived from agricultural waste, making it both sustainable and cost-effective to produce. Initial taste tests have shown no impact on the flavor or texture of treated produce.\n\n"We\'re excited about the potential of this technology to reduce food waste at the consumer level," said Dr. James Wilson, lead researcher on the project. "If we can extend the shelf life of produce by two weeks, it could have a significant impact on household food waste."'
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
id: 3,
|
| 32 |
+
title: 'Global Fund Announces $50M for FLW Reduction Projects',
|
| 33 |
+
excerpt: 'Funding opportunity for pilot projects connecting smallholders to processing facilities.',
|
| 34 |
+
date: '2025-10-05',
|
| 35 |
+
category: 'Finance',
|
| 36 |
+
content: 'The Global Food Loss Reduction Fund has announced a new call for proposals with $50 million available for innovative projects that connect smallholder farmers to processing and distribution networks. Priority will be given to projects that demonstrate measurable impact on reducing food loss while improving farmer incomes. Applications are due by December 31, 2025.\n\nThe fund is seeking projects that address food loss at multiple points in the supply chain, from harvest to retail. Preference will be given to initiatives that incorporate innovative technologies, build local capacity, and demonstrate potential for scalability.\n\n"Reducing food loss is not just about preventing waste - it\'s about creating more efficient and equitable food systems," said Fund Director Sarah Johnson. "We\'re looking for projects that can show real impact on both food security and farmer livelihoods."'
|
| 37 |
+
}
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
const item = mockNews.find(news => news.id === parseInt(id));
|
| 41 |
+
|
| 42 |
+
if (item) {
|
| 43 |
+
setTimeout(() => {
|
| 44 |
+
setNewsItem(item);
|
| 45 |
+
setLoading(false);
|
| 46 |
+
}, 300);
|
| 47 |
+
} else {
|
| 48 |
+
setError('News item not found');
|
| 49 |
+
setLoading(false);
|
| 50 |
+
}
|
| 51 |
+
}, [id]);
|
| 52 |
+
|
| 53 |
+
if (loading) {
|
| 54 |
+
return (
|
| 55 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 56 |
+
<div className="container mx-auto px-4">
|
| 57 |
+
<div className="flex justify-center">
|
| 58 |
+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-bio-green"></div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
if (error) {
|
| 66 |
+
return (
|
| 67 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 68 |
+
<div className="container mx-auto px-4">
|
| 69 |
+
<div className="text-center">
|
| 70 |
+
<h2 className="text-2xl font-bold text-red-500 mb-4 drop-shadow">Error</h2>
|
| 71 |
+
<p className="text-red-500 drop-shadow">{error}</p>
|
| 72 |
+
<Link to="/news" className="mt-4 inline-block text-bio-blue dark:text-blue-400 hover:underline drop-shadow bg-gradient-to-r from-bio-blue to-blue-500 bg-clip-text text-transparent">
|
| 73 |
+
← Back to News
|
| 74 |
+
</Link>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return (
|
| 82 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 83 |
+
<div className="container mx-auto px-4">
|
| 84 |
+
<Link to="/news" className="inline-block text-bio-blue dark:text-blue-400 hover:underline mb-6 drop-shadow bg-gradient-to-r from-bio-blue to-blue-500 bg-clip-text text-transparent">
|
| 85 |
+
← Back to News
|
| 86 |
+
</Link>
|
| 87 |
+
|
| 88 |
+
<article className="bg-white dark:bg-gray-700 rounded-2xl shadow-lg overflow-hidden max-w-4xl mx-auto border border-gray-100 dark:border-gray-600 bg-gradient-to-br from-white to-gray-100 dark:from-gray-700 dark:to-gray-800">
|
| 89 |
+
<div className="p-6 md:p-8">
|
| 90 |
+
<div className="flex flex-wrap items-center justify-between mb-6">
|
| 91 |
+
<span className="inline-block px-3 py-1 text-xs font-semibold text-bio-green bg-green-100 dark:bg-green-900/30 dark:text-green-300 rounded-full drop-shadow bg-gradient-to-r from-green-100 to-green-200 dark:from-green-900/30 dark:to-green-800/30">
|
| 92 |
+
{newsItem.category}
|
| 93 |
+
</span>
|
| 94 |
+
<span className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">
|
| 95 |
+
{new Date(newsItem.date).toLocaleDateString('en-US', {
|
| 96 |
+
year: 'numeric',
|
| 97 |
+
month: 'long',
|
| 98 |
+
day: 'numeric'
|
| 99 |
+
})}
|
| 100 |
+
</span>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<h1 className="text-3xl md:text-4xl font-bold mb-6 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">
|
| 104 |
+
{newsItem.title}
|
| 105 |
+
</h1>
|
| 106 |
+
|
| 107 |
+
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 italic drop-shadow bg-gradient-to-r from-gray-600 to-gray-500 bg-clip-text text-transparent dark:from-gray-300 dark:to-gray-400">
|
| 108 |
+
{newsItem.excerpt}
|
| 109 |
+
</p>
|
| 110 |
+
|
| 111 |
+
<div className="prose prose-lg dark:prose-invert max-w-none">
|
| 112 |
+
{newsItem.content.split('\n\n').map((paragraph, index) => (
|
| 113 |
+
<p key={index} className="mb-4 text-gray-700 dark:text-gray-300 drop-shadow">
|
| 114 |
+
{paragraph}
|
| 115 |
+
</p>
|
| 116 |
+
))}
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</article>
|
| 120 |
+
|
| 121 |
+
<div className="max-w-4xl mx-auto mt-8">
|
| 122 |
+
<div className="bg-gradient-to-r from-bio-green to-bio-blue rounded-2xl p-6 text-white shadow-lg">
|
| 123 |
+
<h3 className="text-xl font-bold mb-2 drop-shadow">Stay Informed</h3>
|
| 124 |
+
<p className="mb-4 drop-shadow">Subscribe to our newsletter to receive updates on food loss and waste reduction initiatives.</p>
|
| 125 |
+
<div className="flex flex-col sm:flex-row gap-4">
|
| 126 |
+
<input
|
| 127 |
+
type="email"
|
| 128 |
+
placeholder="Your email address"
|
| 129 |
+
className="flex-grow px-4 py-2 rounded-lg text-gray-800 drop-shadow"
|
| 130 |
+
/>
|
| 131 |
+
<button className="bg-white text-bio-green font-bold px-6 py-2 rounded-lg hover:bg-green-100 transition shadow-md drop-shadow">
|
| 132 |
+
Subscribe
|
| 133 |
+
</button>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
);
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
export default NewsDetailPage;
|
src/pages/NewsPage.jsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const NewsPage = () => {
|
| 5 |
+
const [news, setNews] = useState([]);
|
| 6 |
+
const [loading, setLoading] = useState(true);
|
| 7 |
+
const [error, setError] = useState(null);
|
| 8 |
+
|
| 9 |
+
useEffect(() => {
|
| 10 |
+
// In a real app, this would fetch from an API
|
| 11 |
+
// For demo purposes, we'll use mock data
|
| 12 |
+
const mockNews = [
|
| 13 |
+
{
|
| 14 |
+
id: 1,
|
| 15 |
+
title: 'New Cold Chain Initiative Launched in Southeast Asia',
|
| 16 |
+
excerpt: 'Regional partnership aims to reduce post-harvest losses by 30% through solar-powered refrigeration.',
|
| 17 |
+
date: '2025-10-15',
|
| 18 |
+
category: 'Policy',
|
| 19 |
+
content: 'The Southeast Asian Cold Chain Initiative represents a significant step forward in addressing post-harvest losses in the region. The project, funded by a coalition of international donors, will install solar-powered refrigeration units in rural areas where access to electricity is limited. Early pilot programs have shown promising results, with participating farms reporting a 25% reduction in losses of perishable goods.'
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
id: 2,
|
| 23 |
+
title: 'Innovative Edible Coatings Show Promise in Lab Trials',
|
| 24 |
+
excerpt: 'New biodegradable coatings extend shelf life of fruits by up to 2 weeks.',
|
| 25 |
+
date: '2025-10-10',
|
| 26 |
+
category: 'Technology',
|
| 27 |
+
content: 'Scientists at the Institute for Sustainable Agriculture have developed a new type of edible coating derived from plant-based materials that can extend the shelf life of fruits and vegetables. In laboratory trials, apples treated with the coating remained fresh for up to 14 days longer than untreated controls. The coating is completely biodegradable and safe for consumption.'
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
id: 3,
|
| 31 |
+
title: 'Global Fund Announces $50M for FLW Reduction Projects',
|
| 32 |
+
excerpt: 'Funding opportunity for pilot projects connecting smallholders to processing facilities.',
|
| 33 |
+
date: '2025-10-05',
|
| 34 |
+
category: 'Finance',
|
| 35 |
+
content: 'The Global Food Loss Reduction Fund has announced a new call for proposals with $50 million available for innovative projects that connect smallholder farmers to processing and distribution networks. Priority will be given to projects that demonstrate measurable impact on reducing food loss while improving farmer incomes. Applications are due by December 31, 2025.'
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
id: 4,
|
| 39 |
+
title: 'Mobile Processing Units Deployed in Rural Africa',
|
| 40 |
+
excerpt: 'New program brings food processing capabilities directly to farming communities.',
|
| 41 |
+
date: '2025-09-28',
|
| 42 |
+
category: 'Implementation',
|
| 43 |
+
content: 'A new program launched by the African Agricultural Development Initiative is deploying mobile processing units to rural farming communities. These units can process fruits, vegetables, and grains on-site, reducing the need for long transportation to centralized facilities. Early results show a 40% reduction in post-harvest losses in participating communities.'
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
id: 5,
|
| 47 |
+
title: 'Blockchain Technology Enhances Supply Chain Transparency',
|
| 48 |
+
excerpt: 'New platform tracks food from farm to consumer, reducing waste through better coordination.',
|
| 49 |
+
date: '2025-09-20',
|
| 50 |
+
category: 'Technology',
|
| 51 |
+
content: 'A consortium of technology companies and agricultural organizations has launched a blockchain-based platform that tracks food products from farm to consumer. The system provides real-time visibility into inventory levels, expiration dates, and distribution routes, enabling better coordination and reducing waste throughout the supply chain.'
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
id: 6,
|
| 55 |
+
title: 'Community Composting Programs Expand Globally',
|
| 56 |
+
excerpt: 'Cities adopt innovative approaches to turn food waste into valuable resources.',
|
| 57 |
+
date: '2025-09-15',
|
| 58 |
+
category: 'Circular Economy',
|
| 59 |
+
content: 'Cities around the world are implementing innovative community composting programs that turn food waste into valuable soil amendments. These programs not only reduce waste sent to landfills but also provide local communities with a source of high-quality compost for urban agriculture and gardening.'
|
| 60 |
+
}
|
| 61 |
+
];
|
| 62 |
+
|
| 63 |
+
setTimeout(() => {
|
| 64 |
+
setNews(mockNews);
|
| 65 |
+
setLoading(false);
|
| 66 |
+
}, 500);
|
| 67 |
+
}, []);
|
| 68 |
+
|
| 69 |
+
if (loading) {
|
| 70 |
+
return (
|
| 71 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 72 |
+
<div className="container mx-auto px-4">
|
| 73 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">News & Updates</h2>
|
| 74 |
+
<div className="flex justify-center">
|
| 75 |
+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-bio-green"></div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if (error) {
|
| 83 |
+
return (
|
| 84 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 85 |
+
<div className="container mx-auto px-4">
|
| 86 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">News & Updates</h2>
|
| 87 |
+
<div className="text-center text-red-500">
|
| 88 |
+
<p>Error loading news: {error}</p>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
return (
|
| 96 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen">
|
| 97 |
+
<div className="container mx-auto px-4">
|
| 98 |
+
<h2 className="text-3xl font-bold mb-8 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">News & Updates</h2>
|
| 99 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
| 100 |
+
{news.map((item) => (
|
| 101 |
+
<div key={item.id} className="bg-white dark:bg-gray-700 rounded-2xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 border border-gray-100 dark:border-gray-600 bg-gradient-to-br from-white to-gray-100 dark:from-gray-700 dark:to-gray-800">
|
| 102 |
+
<div className="p-6">
|
| 103 |
+
<div className="flex justify-between items-start mb-4">
|
| 104 |
+
<span className="inline-block px-3 py-1 text-xs font-semibold text-bio-green bg-green-100 dark:bg-green-900/30 dark:text-green-300 rounded-full drop-shadow bg-gradient-to-r from-green-100 to-green-200 dark:from-green-900/30 dark:to-green-800/30">
|
| 105 |
+
{item.category}
|
| 106 |
+
</span>
|
| 107 |
+
<span className="text-sm text-gray-500 dark:text-gray-400 drop-shadow">{new Date(item.date).toLocaleDateString()}</span>
|
| 108 |
+
</div>
|
| 109 |
+
<h3 className="text-xl font-bold mb-3 text-gray-800 dark:text-white drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">{item.title}</h3>
|
| 110 |
+
<p className="text-gray-600 dark:text-gray-300 mb-4 drop-shadow">{item.excerpt}</p>
|
| 111 |
+
<Link to={`/news/${item.id}`} className="text-bio-blue dark:text-blue-400 font-medium hover:text-blue-700 dark:hover:text-blue-300 transition drop-shadow bg-gradient-to-r from-bio-blue to-blue-500 bg-clip-text text-transparent">
|
| 112 |
+
Read More →
|
| 113 |
+
</Link>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
))}
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
);
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
export default NewsPage;
|
src/pages/NotFoundPage.jsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
const NotFoundPage = () => {
|
| 5 |
+
return (
|
| 6 |
+
<div className="py-16 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 transition-colors duration-300 min-h-screen flex items-center">
|
| 7 |
+
<div className="container mx-auto px-4 text-center">
|
| 8 |
+
<h1 className="text-6xl font-bold text-bio-green dark:text-green-400 mb-4 drop-shadow bg-gradient-to-r from-bio-green to-bio-blue bg-clip-text text-transparent">404</h1>
|
| 9 |
+
<h2 className="text-3xl font-bold text-gray-800 dark:text-white mb-6 drop-shadow bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent dark:from-white dark:to-gray-300">Page Not Found</h2>
|
| 10 |
+
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto drop-shadow bg-gradient-to-r from-gray-600 to-gray-500 bg-clip-text text-transparent dark:from-gray-300 dark:to-gray-400">
|
| 11 |
+
Sorry, the page you're looking for doesn't exist or has been moved.
|
| 12 |
+
</p>
|
| 13 |
+
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
| 14 |
+
<Link
|
| 15 |
+
to="/"
|
| 16 |
+
className="bg-gradient-to-r from-bio-green to-green-500 text-white font-bold py-3 px-8 rounded-lg hover:from-green-500 hover:to-green-600 transition shadow-lg text-center drop-shadow"
|
| 17 |
+
>
|
| 18 |
+
Go Home
|
| 19 |
+
</Link>
|
| 20 |
+
<Link
|
| 21 |
+
to="/directory"
|
| 22 |
+
className="bg-gradient-to-r from-bio-purple to-purple-500 text-white font-bold py-3 px-8 rounded-lg hover:from-purple-500 hover:to-purple-600 transition shadow-lg text-center drop-shadow"
|
| 23 |
+
>
|
| 24 |
+
Browse Directory
|
| 25 |
+
</Link>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
export default NotFoundPage;
|
src/utils/theme.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const applyTheme = (theme) => {
|
| 2 |
+
const root = document.documentElement;
|
| 3 |
+
|
| 4 |
+
if (theme === 'dark') {
|
| 5 |
+
root.classList.add('dark');
|
| 6 |
+
} else {
|
| 7 |
+
root.classList.remove('dark');
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
// Save to localStorage
|
| 11 |
+
localStorage.setItem('theme', theme);
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export const getInitialTheme = () => {
|
| 15 |
+
const savedTheme = localStorage.getItem('theme');
|
| 16 |
+
const prefersDark = window.matchMedia &&
|
| 17 |
+
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| 18 |
+
|
| 19 |
+
if (savedTheme) {
|
| 20 |
+
return savedTheme;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
return prefersDark ? 'dark' : 'light';
|
| 24 |
+
};
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
darkMode: 'class', // Enable class-based dark mode
|
| 8 |
+
theme: {
|
| 9 |
+
extend: {
|
| 10 |
+
colors: {
|
| 11 |
+
'bio-green': '#16a34a',
|
| 12 |
+
'bio-blue': '#2563eb',
|
| 13 |
+
'bio-purple': '#9333ea',
|
| 14 |
+
'bio-orange': '#ea580c',
|
| 15 |
+
'bio-yellow': '#ca8a04',
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
},
|
| 19 |
+
plugins: [],
|
| 20 |
+
}
|
vite.config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vitejs.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
server: {
|
| 8 |
+
host: '0.0.0.0',
|
| 9 |
+
port: 5173,
|
| 10 |
+
},
|
| 11 |
+
build: {
|
| 12 |
+
outDir: 'dist',
|
| 13 |
+
assetsDir: 'assets',
|
| 14 |
+
rollupOptions: {
|
| 15 |
+
output: {
|
| 16 |
+
manualChunks: {
|
| 17 |
+
vendor: ['react', 'react-dom', 'react-router-dom'],
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
})
|