first commit

This commit is contained in:
Mateusz Gruszczyński 2025-03-07 17:32:02 +01:00
commit 7facd84b99
10 changed files with 634 additions and 0 deletions

0
README.md Normal file
View File

140
backend/index.js Normal file
View File

@ -0,0 +1,140 @@
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const app = express();
app.use(cors());
app.use(bodyParser.json());
const PORT = 4000;
const ADMIN_LOGIN = 'admin';
const ADMIN_PASSWORD = 'admin123';
const db = new sqlite3.Database('./donations.db', (err) => {
if (err) {
console.error('Błąd otwarcia bazy danych', err);
} else {
console.log('Połączono z bazą SQLite.');
}
});
// Tworzenie tabel: campaigns oraz donations
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS campaigns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL,
target REAL NOT NULL
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS donations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL,
amount REAL NOT NULL,
description TEXT NOT NULL,
date TEXT NOT NULL,
FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
)
`);
});
// -----------------------
// Endpointy publiczne
// -----------------------
// Lista wszystkich kampanii
app.get('/api/campaigns', (req, res) => {
db.all("SELECT * FROM campaigns", (err, rows) => {
if (err) return res.status(500).json({ error: 'Błąd bazy danych' });
res.json(rows);
});
});
// Pobranie szczegółów kampanii (razem z sumą wpłat)
app.get('/api/campaigns/:id', (req, res) => {
const campaignId = req.params.id;
db.get("SELECT * FROM campaigns WHERE id = ?", [campaignId], (err, campaign) => {
if (err) return res.status(500).json({ error: 'Błąd bazy danych' });
if (!campaign) return res.status(404).json({ error: 'Kampania nie znaleziona' });
db.get(
"SELECT SUM(amount) as totalDonations FROM donations WHERE campaign_id = ?",
[campaignId],
(err, row) => {
if (err) return res.status(500).json({ error: 'Błąd bazy danych' });
const totalDonations = row.totalDonations || 0;
res.json({ ...campaign, totalDonations });
}
);
});
});
// Lista wpłat dla kampanii
app.get('/api/campaigns/:id/donations', (req, res) => {
const campaignId = req.params.id;
db.all("SELECT * FROM donations WHERE campaign_id = ? ORDER BY date DESC", [campaignId], (err, rows) => {
if (err) return res.status(500).json({ error: 'Błąd bazy danych' });
res.json(rows);
});
});
// -----------------------
// Endpointy chronione panel administratora
// -----------------------
// Logowanie bardzo uproszczone
app.post('/api/login', (req, res) => {
const { login, password } = req.body;
if (login === ADMIN_LOGIN && password === ADMIN_PASSWORD) {
res.json({ token: 'admin-session-token-abc123' });
} else {
res.status(401).json({ error: 'Błędne dane logowania' });
}
});
// Tworzenie nowej kampanii (admin)
app.post('/api/campaigns', (req, res) => {
const { title, description, target, token } = req.body;
if (!token) return res.status(401).json({ error: 'Brak uprawnień' });
const stmt = db.prepare("INSERT INTO campaigns (title, description, target) VALUES (?, ?, ?)");
stmt.run(title, description, target, function(err) {
if (err) return res.status(500).json({ error: 'Błąd bazy danych' });
const newCampaign = { id: this.lastID, title, description, target };
res.status(201).json(newCampaign);
});
stmt.finalize();
});
// Aktualizacja celu kampanii (admin)
app.post('/api/campaigns/:id/target', (req, res) => {
const campaignId = req.params.id;
const { newTarget, token } = req.body;
if (!token) return res.status(401).json({ error: 'Brak uprawnień' });
db.run("UPDATE campaigns SET target = ? WHERE id = ?", [newTarget, campaignId], function(err) {
if (err) return res.status(500).json({ error: 'Błąd bazy danych' });
res.json({ target: newTarget });
});
});
// Dodawanie wpłaty do kampanii (admin)
app.post('/api/campaigns/:id/donations', (req, res) => {
const campaignId = req.params.id;
const { amount, description, token } = req.body;
if (!token) return res.status(401).json({ error: 'Brak uprawnień' });
const date = new Date().toISOString();
const stmt = db.prepare("INSERT INTO donations (campaign_id, amount, description, date) VALUES (?, ?, ?, ?)");
stmt.run(campaignId, amount, description, date, function(err) {
if (err) return res.status(500).json({ error: 'Błąd bazy danych' });
const newDonation = { id: this.lastID, campaign_id: campaignId, amount, description, date };
res.status(201).json(newDonation);
});
stmt.finalize();
});
app.listen(PORT, () => {
console.log(`Server działa na porcie ${PORT}`);
});

15
backend/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "donation-backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"sqlite3": "^5.1.6"
}
}

18
frontend/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "donation-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}

205
frontend/src/Admin.js Normal file
View File

@ -0,0 +1,205 @@
// Admin.js
import React, { useState, useEffect } from 'react';
import { useNavigate, Routes, Route, Link, useParams } from 'react-router-dom';
import './App.css';
function AdminHome() {
const [campaigns, setCampaigns] = useState([]);
useEffect(() => {
fetch('http://localhost:4000/api/campaigns')
.then((res) => res.json())
.then((data) => setCampaigns(data))
.catch((err) => console.error(err));
}, []);
return (
<div className="container">
<h1>Panel Administratora</h1>
<Link to="/admin/new" className="button" style={{ marginBottom: '20px', display: 'inline-block' }}>
Dodaj nową zbiórkę
</Link>
<h2>Zbiórki</h2>
<ul>
{campaigns.map((c) => (
<li key={c.id} style={{ marginBottom: '10px' }}>
<strong>{c.title}</strong> Cel: {c.target} zł{' '}
<Link to={`/admin/campaign/${c.id}`} className="button" style={{ marginLeft: '10px' }}>
Zarządzaj
</Link>
</li>
))}
</ul>
</div>
);
}
function NewCampaign() {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [target, setTarget] = useState('');
const navigate = useNavigate();
const handleSubmit = (e) => {
e.preventDefault();
const token = sessionStorage.getItem('authToken');
fetch('http://localhost:4000/api/campaigns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description, target: parseFloat(target), token }),
})
.then((res) => {
if (!res.ok) throw new Error('Błąd tworzenia zbiórki');
return res.json();
})
.then(() => {
alert('Zbiórka dodana');
navigate('/admin');
})
.catch((err) => alert('Błąd podczas dodawania zbiórki'));
};
return (
<div className="container">
<div className="card">
<h2>Dodaj nową zbiórkę</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Tytuł:</label>
<input type="text" value={title} onChange={(e) => setTitle(e.target.value)} required />
</div>
<div className="form-group">
<label>Opis:</label>
<textarea value={description} onChange={(e) => setDescription(e.target.value)} required />
</div>
<div className="form-group">
<label>Cel ():</label>
<input type="number" value={target} onChange={(e) => setTarget(e.target.value)} required />
</div>
<button type="submit" className="button">Dodaj zbiórkę</button>
</form>
</div>
</div>
);
}
function AdminCampaignDetail() {
const { campaignId } = useParams();
const [campaign, setCampaign] = useState(null);
const [donations, setDonations] = useState([]);
const [newTarget, setNewTarget] = useState('');
const [donationAmount, setDonationAmount] = useState('');
const [donationDesc, setDonationDesc] = useState('');
useEffect(() => {
fetch(`http://localhost:4000/api/campaigns/${campaignId}`)
.then((res) => res.json())
.then((data) => setCampaign(data))
.catch((err) => console.error(err));
fetch(`http://localhost:4000/api/campaigns/${campaignId}/donations`)
.then((res) => res.json())
.then((data) => setDonations(data))
.catch((err) => console.error(err));
}, [campaignId]);
const handleUpdateTarget = (e) => {
e.preventDefault();
const token = sessionStorage.getItem('authToken');
fetch(`http://localhost:4000/api/campaigns/${campaignId}/target`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newTarget: parseFloat(newTarget), token }),
})
.then((res) => {
if (!res.ok) throw new Error('Błąd aktualizacji celu');
return res.json();
})
.then((data) => {
setCampaign({ ...campaign, target: data.target });
setNewTarget('');
alert('Cel zaktualizowany');
})
.catch((err) => alert('Błąd podczas aktualizacji celu'));
};
const handleAddDonation = (e) => {
e.preventDefault();
const token = sessionStorage.getItem('authToken');
fetch(`http://localhost:4000/api/campaigns/${campaignId}/donations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: parseFloat(donationAmount), description: donationDesc, token }),
})
.then((res) => {
if (!res.ok) throw new Error('Błąd dodawania wpłaty');
return res.json();
})
.then((newDonation) => {
setDonations([newDonation, ...donations]);
setDonationAmount('');
setDonationDesc('');
})
.catch((err) => alert('Błąd podczas dodawania wpłaty'));
};
if (!campaign) return <div>Ładowanie...</div>;
return (
<div className="container">
<div className="card">
<h2>Zbiórka: {campaign.title}</h2>
<p>{campaign.description}</p>
<p>
Cel: <strong>{campaign.target} </strong> | Zebrano: <strong>{campaign.totalDonations} zł</strong>
</p>
<form onSubmit={handleUpdateTarget}>
<div className="form-group">
<label>Aktualizuj cel:</label>
<input type="number" value={newTarget} onChange={(e) => setNewTarget(e.target.value)} required />
</div>
<button type="submit" className="button">Aktualizuj cel</button>
</form>
<h3>Dodaj wpłatę</h3>
<form onSubmit={handleAddDonation}>
<div className="form-group">
<label>Kwota:</label>
<input type="number" value={donationAmount} onChange={(e) => setDonationAmount(e.target.value)} required />
</div>
<div className="form-group">
<label>Opis:</label>
<input type="text" value={donationDesc} onChange={(e) => setDonationDesc(e.target.value)} required />
</div>
<button type="submit" className="button">Dodaj wpłatę</button>
</form>
<h3>Historia wpłat</h3>
<ul>
{donations.map((d) => (
<li key={d.id}>
Kwota: {d.amount} , {d.description}, {new Date(d.date).toLocaleString()}
</li>
))}
</ul>
</div>
</div>
);
}
function Admin() {
const navigate = useNavigate();
useEffect(() => {
const token = sessionStorage.getItem('authToken');
if (!token) navigate('/login');
}, [navigate]);
return (
<Routes>
<Route path="/" element={<AdminHome />} />
<Route path="/new" element={<NewCampaign />} />
<Route path="/campaign/:campaignId" element={<AdminCampaignDetail />} />
</Routes>
);
}
export default Admin;

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

@ -0,0 +1,87 @@
/* App.css */
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.card {
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.progress-container {
background-color: #e0e0e0;
border-radius: 20px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar {
height: 30px;
background-color: #76c7c0;
width: 0;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
transition: width 0.5s ease-in-out;
}
.button {
background-color: #76c7c0;
color: #fff;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
.button:hover {
background-color: #5aa9a4;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
@media (max-width: 600px) {
.container {
padding: 10px;
}
.progress-bar {
font-size: 14px;
}
}

23
frontend/src/App.js Normal file
View File

@ -0,0 +1,23 @@
// App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import HomePage from './HomePage';
import CampaignDetail from './CampaignDetail';
import Login from './Login';
import Admin from './Admin';
import './App.css';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/campaign/:id" element={<CampaignDetail />} />
<Route path="/login" element={<Login />} />
<Route path="/admin/*" element={<Admin />} />
</Routes>
</Router>
);
}
export default App;

View File

@ -0,0 +1,48 @@
// CampaignDetail.js
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import ProgressBar from './ProgressBar';
import './App.css';
function CampaignDetail() {
const { id } = useParams();
const [campaign, setCampaign] = useState(null);
const [donations, setDonations] = useState([]);
useEffect(() => {
fetch(`http://localhost:4000/api/campaigns/${id}`)
.then((res) => res.json())
.then((data) => setCampaign(data))
.catch((err) => console.error(err));
fetch(`http://localhost:4000/api/campaigns/${id}/donations`)
.then((res) => res.json())
.then((data) => setDonations(data))
.catch((err) => console.error(err));
}, [id]);
if (!campaign) return <div>Ładowanie...</div>;
return (
<div className="container">
<div className="card">
<h1>{campaign.title}</h1>
<p>{campaign.description}</p>
<ProgressBar value={campaign.totalDonations} max={campaign.target} />
<p style={{ textAlign: 'center' }}>
<strong>{campaign.totalDonations} </strong> z <strong>{campaign.target} zł</strong>
</p>
<h2>Ostatnie wpłaty</h2>
<ul>
{donations.map((d) => (
<li key={d.id}>
Kwota: {d.amount} {d.description} {new Date(d.date).toLocaleString()}
</li>
))}
</ul>
</div>
</div>
);
}
export default CampaignDetail;

34
frontend/src/HomePage.js Normal file
View File

@ -0,0 +1,34 @@
// HomePage.js
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import './App.css';
function HomePage() {
const [campaigns, setCampaigns] = useState([]);
useEffect(() => {
fetch('http://localhost:4000/api/campaigns')
.then((res) => res.json())
.then((data) => setCampaigns(data))
.catch((err) => console.error(err));
}, []);
return (
<div className="container">
<h1 className="header">Lista Zbiórek</h1>
<div className="card">
{campaigns.map((campaign) => (
<div key={campaign.id} style={{ marginBottom: '15px' }}>
<h2>{campaign.title}</h2>
<p>{campaign.description}</p>
<Link to={`/campaign/${campaign.id}`} className="button">
Zobacz szczegóły
</Link>
</div>
))}
</div>
</div>
);
}
export default HomePage;

64
frontend/src/Login.js Normal file
View File

@ -0,0 +1,64 @@
// Login.js
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './App.css';
function Login() {
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleLogin = (e) => {
e.preventDefault();
fetch('http://localhost:4000/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ login, password }),
})
.then((res) => {
if (!res.ok) throw new Error('Błąd logowania');
return res.json();
})
.then((data) => {
sessionStorage.setItem('authToken', data.token);
navigate('/admin');
})
.catch((err) => {
alert('Niepoprawne dane logowania.');
});
};
return (
<div className="container">
<div className="card">
<h2>Logowanie do panelu</h2>
<form onSubmit={handleLogin}>
<div className="form-group">
<label>Login</label>
<input
type="text"
value={login}
onChange={(e) => setLogin(e.target.value)}
required
/>
</div>
<div className="form-group">
<label>Hasło</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className="button">
Zaloguj
</button>
</form>
</div>
</div>
);
}
export default Login;