i finally committed i guess

Signed-off-by: boris <boris@borishub.co.uk>
This commit is contained in:
boris
2025-03-15 01:59:16 +00:00
parent 8de2b7f29e
commit 709596eea2
113 changed files with 25075 additions and 54344 deletions

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# JWT Configuration
JWT_SECRET_KEY=your-secret-key-here
JWT_TOKEN_EXPIRY=3600 # 1 hour in seconds
# Database Configuration
DB_HOST=localhost
DB_NAME=your_database_name
DB_USER=your_database_user
DB_PASS=your_database_password

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Environment variables
.env
.env.local
.env.*.local
# IDE files
.idea/
.vscode/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

1
.idea/Ecobuddy.iml generated
View File

@@ -4,5 +4,6 @@
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="leaflet" level="application" />
</component>
</module>

21
.idea/dataSources.xml generated
View File

@@ -1,11 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="ecobuddy" uuid="b932ada6-ed77-47fa-96d8-d6dfa86a6ca2">
<data-source source="LOCAL" name="ecobuddynew.sqlite" uuid="6566010b-b220-4baf-bb3e-99178c3287f0">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/Databases/ecobuddy.sqlite</jdbc-url>
<jdbc-url>jdbc:sqlite:Databases/ecobuddynew.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="ecobuddynew" uuid="b5d0338c-4f7c-4008-ba23-032fa68749c1">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/Databases/ecobuddynew.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
@@ -14,13 +21,19 @@
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/xerial/sqlite-jdbc/3.45.1.0/sqlite-jdbc-3.45.1.0.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.45.1/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar</url>
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="ecobuddynew.sqlite" uuid="6566010b-b220-4baf-bb3e-99178c3287f0">
<data-source source="LOCAL" name="ecobuddy.sqlite" uuid="5216c958-85d2-48a7-b57e-256771f5c73c">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:Databases/ecobuddynew.sqlite</jdbc-url>
<jdbc-url>jdbc:sqlite:Databases/ecobuddy.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{jquery}" />
<file url="PROJECT" libraries="{leaflet}" />
</component>
</project>

2
.idea/php.xml generated
View File

@@ -10,7 +10,7 @@
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.1" />
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>

0
.idea/sqldialects.xml generated Normal file → Executable file
View File

232
Databases/add_facilities.py Normal file
View File

@@ -0,0 +1,232 @@
import sqlite3
import random
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Check if we need to add any new categories
cursor.execute("SELECT id FROM ecoCategories")
existing_categories = [row[0] for row in cursor.fetchall()]
# Add two new categories
new_categories = [
(16, "Urban Farms"),
(17, "Rainwater Harvesting Systems")
]
for category in new_categories:
if category[0] not in existing_categories:
cursor.execute("INSERT INTO ecoCategories (id, name) VALUES (?, ?)", category)
print(f"Added new category: {category[1]}")
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Define 10 new ecological facilities in the UK with accurate location data
new_facilities = [
{
"title": "Community Garden Hackney",
"category": 12, # Pollinator Gardens
"description": "Urban garden with native plants to support local pollinators",
"houseNumber": "45",
"streetName": "Dalston Lane",
"county": "Greater London",
"town": "London",
"postcode": "E8 3AH",
"lng": -0.0612,
"lat": 51.5476,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded with new wildflower section",
"Volunteer days every Saturday"
]
},
{
"title": "Rooftop Solar Farm",
"category": 8, # Green Roofs
"description": "Combined green roof and solar panel installation on commercial building",
"houseNumber": "120",
"streetName": "Deansgate",
"county": "Greater Manchester",
"town": "Manchester",
"postcode": "M3 2QJ",
"lng": -2.2484,
"lat": 53.4808,
"contributor": random.choice(user_ids),
"status_comments": [
"Generates power for the entire building"
]
},
{
"title": "Edinburgh Tool Library",
"category": 15, # Community Tool Libraries
"description": "Borrow tools instead of buying them - reducing waste and consumption",
"houseNumber": "25",
"streetName": "Leith Walk",
"county": "Edinburgh",
"town": "Edinburgh",
"postcode": "EH6 8LN",
"lng": -3.1752,
"lat": 55.9677,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Cardiff Bay Water Refill Station",
"category": 9, # Public Water Refill Stations
"description": "Free water refill station to reduce plastic bottle usage",
"houseNumber": "3",
"streetName": "Mermaid Quay",
"county": "Cardiff",
"town": "Cardiff",
"postcode": "CF10 5BZ",
"lng": -3.1644,
"lat": 51.4644,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently cleaned and maintained",
"High usage during summer months"
]
},
{
"title": "Bristol Urban Farm",
"category": 16, # Urban Farms (new category)
"description": "Community-run urban farm providing local produce and education",
"houseNumber": "18",
"streetName": "Stapleton Road",
"county": "Bristol",
"town": "Bristol",
"postcode": "BS5 0RA",
"lng": -2.5677,
"lat": 51.4635,
"contributor": random.choice(user_ids),
"status_comments": [
"Open for volunteers Tuesday-Sunday"
]
},
{
"title": "Newcastle Rainwater Collection System",
"category": 17, # Rainwater Harvesting Systems (new category)
"description": "Public demonstration of rainwater harvesting for garden irrigation",
"houseNumber": "55",
"streetName": "Northumberland Street",
"county": "Tyne and Wear",
"town": "Newcastle upon Tyne",
"postcode": "NE1 7DH",
"lng": -1.6178,
"lat": 54.9783,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Brighton Beach Solar Bench",
"category": 7, # Solar-Powered Benches
"description": "Solar-powered bench with USB charging ports and WiFi",
"houseNumber": "",
"streetName": "Kings Road",
"county": "East Sussex",
"town": "Brighton",
"postcode": "BN1 2FN",
"lng": -0.1426,
"lat": 50.8214,
"contributor": random.choice(user_ids),
"status_comments": [
"Popular spot for tourists",
"One USB port currently not working"
]
},
{
"title": "Leeds Community Compost Hub",
"category": 6, # Community Compost Bins
"description": "Large-scale community composting facility for local residents",
"houseNumber": "78",
"streetName": "Woodhouse Lane",
"county": "West Yorkshire",
"town": "Leeds",
"postcode": "LS2 9JT",
"lng": -1.5491,
"lat": 53.8067,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded capacity"
]
},
{
"title": "Glasgow EV Charging Hub",
"category": 4, # Public EV Charging Stations
"description": "Multi-vehicle EV charging station with fast chargers",
"houseNumber": "42",
"streetName": "Buchanan Street",
"county": "Glasgow",
"town": "Glasgow",
"postcode": "G1 3JX",
"lng": -4.2526,
"lat": 55.8621,
"contributor": random.choice(user_ids),
"status_comments": [
"6 charging points available",
"24/7 access"
]
},
{
"title": "Oxford E-Waste Collection Center",
"category": 13, # E-Waste Collection Bins
"description": "Dedicated facility for proper disposal and recycling of electronic waste",
"houseNumber": "15",
"streetName": "St Aldate's",
"county": "Oxfordshire",
"town": "Oxford",
"postcode": "OX1 1BX",
"lng": -1.2577,
"lat": 51.7520,
"contributor": random.choice(user_ids),
"status_comments": []
}
]
# Insert facilities into the database
for facility in new_facilities:
cursor.execute("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
facility["title"],
facility["category"],
facility["description"],
facility["houseNumber"],
facility["streetName"],
facility["county"],
facility["town"],
facility["postcode"],
facility["lng"],
facility["lat"],
facility["contributor"]
))
# Get the ID of the inserted facility
facility_id = cursor.lastrowid
# Add status comments if any
for comment in facility["status_comments"]:
cursor.execute("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", (facility_id, comment))
print(f"Added facility: {facility['title']} in {facility['town']}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print("\nSuccessfully added 10 new ecological facilities to the database.")
Using the test data in the ecoCategories, ecoFacilities, and ecoFacilityStatus, please create 10 new ecological facilities with the following constraints:
Must be located in the United Kingdom (Names of areas, towns, cities, counties)
Postcode, Latitude and Longitude must be fairly accurate to the location you have generated for the area.
More categories may be made, it is up to you, but make sure foreign keys are respected.
Contributor may be of any user in the database.
Not all facilities must have statusComments, however facilities may have multiple statusComments.

BIN
Databases/ecobuddy.sqlite Executable file → Normal file

Binary file not shown.

BIN
Databases/ecobuddynew.sqlite Normal file → Executable file

Binary file not shown.

View File

@@ -0,0 +1,66 @@
Starting facility generation at 2025-03-14 22:20:38.302675
Target: 1000 new facilities
Generating facilities...
Generated 100 facilities so far...
Generated 200 facilities so far...
Generated 300 facilities so far...
Generated 400 facilities so far...
Generated 500 facilities so far...
Generated 600 facilities so far...
Generated 700 facilities so far...
Generated 800 facilities so far...
Generated 900 facilities so far...
Generated 1000 facilities so far...
Inserting facilities into database...
Inserted batch 1/20
Inserted batch 2/20
Inserted batch 3/20
Inserted batch 4/20
Inserted batch 5/20
Inserted batch 6/20
Inserted batch 7/20
Inserted batch 8/20
Inserted batch 9/20
Inserted batch 10/20
Inserted batch 11/20
Inserted batch 12/20
Inserted batch 13/20
Inserted batch 14/20
Inserted batch 15/20
Inserted batch 16/20
Inserted batch 17/20
Inserted batch 18/20
Inserted batch 19/20
Inserted batch 20/20
Inserting status comments...
Inserted comment batch 1/23
Inserted comment batch 2/23
Inserted comment batch 3/23
Inserted comment batch 4/23
Inserted comment batch 5/23
Inserted comment batch 6/23
Inserted comment batch 7/23
Inserted comment batch 8/23
Inserted comment batch 9/23
Inserted comment batch 10/23
Inserted comment batch 11/23
Inserted comment batch 12/23
Inserted comment batch 13/23
Inserted comment batch 14/23
Inserted comment batch 15/23
Inserted comment batch 16/23
Inserted comment batch 17/23
Inserted comment batch 18/23
Inserted comment batch 19/23
Inserted comment batch 20/23
Inserted comment batch 21/23
Inserted comment batch 22/23
Inserted comment batch 23/23
Generation complete at 2025-03-14 22:20:38.336715
Total facilities in database: 1030
Total status comments in database: 1147
Generated facilities saved to generated_facilities.csv for reference

View File

@@ -0,0 +1,568 @@
import sqlite3
import random
import csv
import os
from datetime import datetime
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Get current max facility ID
cursor.execute("SELECT MAX(id) FROM ecoFacilities")
max_facility_id = cursor.fetchone()[0] or 0
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Get list of categories
cursor.execute("SELECT id, name FROM ecoCategories")
categories = {row[0]: row[1] for row in cursor.fetchall()}
# UK Cities and Towns with their counties and approximate coordinates
uk_locations = [
# Format: Town/City, County, Latitude, Longitude, Postcode Area
("London", "Greater London", 51.5074, -0.1278, "EC"),
("Birmingham", "West Midlands", 52.4862, -1.8904, "B"),
("Manchester", "Greater Manchester", 53.4808, -2.2426, "M"),
("Glasgow", "Glasgow", 55.8642, -4.2518, "G"),
("Liverpool", "Merseyside", 53.4084, -2.9916, "L"),
("Bristol", "Bristol", 51.4545, -2.5879, "BS"),
("Edinburgh", "Edinburgh", 55.9533, -3.1883, "EH"),
("Leeds", "West Yorkshire", 53.8008, -1.5491, "LS"),
("Sheffield", "South Yorkshire", 53.3811, -1.4701, "S"),
("Newcastle upon Tyne", "Tyne and Wear", 54.9783, -1.6178, "NE"),
("Nottingham", "Nottinghamshire", 52.9548, -1.1581, "NG"),
("Cardiff", "Cardiff", 51.4816, -3.1791, "CF"),
("Belfast", "Belfast", 54.5973, -5.9301, "BT"),
("Brighton", "East Sussex", 50.8225, -0.1372, "BN"),
("Leicester", "Leicestershire", 52.6369, -1.1398, "LE"),
("Aberdeen", "Aberdeen", 57.1497, -2.0943, "AB"),
("Portsmouth", "Hampshire", 50.8198, -1.0880, "PO"),
("York", "North Yorkshire", 53.9599, -1.0873, "YO"),
("Swansea", "Swansea", 51.6214, -3.9436, "SA"),
("Oxford", "Oxfordshire", 51.7520, -1.2577, "OX"),
("Cambridge", "Cambridgeshire", 52.2053, 0.1218, "CB"),
("Exeter", "Devon", 50.7184, -3.5339, "EX"),
("Bath", "Somerset", 51.3751, -2.3617, "BA"),
("Reading", "Berkshire", 51.4543, -0.9781, "RG"),
("Preston", "Lancashire", 53.7632, -2.7031, "PR"),
("Coventry", "West Midlands", 52.4068, -1.5197, "CV"),
("Hull", "East Yorkshire", 53.7676, -0.3274, "HU"),
("Stoke-on-Trent", "Staffordshire", 53.0027, -2.1794, "ST"),
("Wolverhampton", "West Midlands", 52.5870, -2.1288, "WV"),
("Plymouth", "Devon", 50.3755, -4.1427, "PL"),
("Derby", "Derbyshire", 52.9225, -1.4746, "DE"),
("Sunderland", "Tyne and Wear", 54.9069, -1.3830, "SR"),
("Southampton", "Hampshire", 50.9097, -1.4044, "SO"),
("Norwich", "Norfolk", 52.6309, 1.2974, "NR"),
("Bournemouth", "Dorset", 50.7192, -1.8808, "BH"),
("Middlesbrough", "North Yorkshire", 54.5742, -1.2350, "TS"),
("Blackpool", "Lancashire", 53.8175, -3.0357, "FY"),
("Bolton", "Greater Manchester", 53.5785, -2.4299, "BL"),
("Ipswich", "Suffolk", 52.0567, 1.1482, "IP"),
("Telford", "Shropshire", 52.6784, -2.4453, "TF"),
("Dundee", "Dundee", 56.4620, -2.9707, "DD"),
("Peterborough", "Cambridgeshire", 52.5695, -0.2405, "PE"),
("Huddersfield", "West Yorkshire", 53.6458, -1.7850, "HD"),
("Luton", "Bedfordshire", 51.8787, -0.4200, "LU"),
("Warrington", "Cheshire", 53.3900, -2.5970, "WA"),
("Southend-on-Sea", "Essex", 51.5459, 0.7077, "SS"),
("Swindon", "Wiltshire", 51.5557, -1.7797, "SN"),
("Slough", "Berkshire", 51.5105, -0.5950, "SL"),
("Watford", "Hertfordshire", 51.6565, -0.3903, "WD"),
("Carlisle", "Cumbria", 54.8952, -2.9335, "CA")
]
# Street name components for generating realistic street names
street_prefixes = ["High", "Main", "Church", "Park", "Mill", "Station", "London", "Victoria", "Queen", "King", "North", "South", "East", "West", "New", "Old", "Castle", "Bridge", "Green", "Market", "School", "Manor", "Abbey", "Priory", "Cathedral", "University", "College", "Hospital", "Railway", "Canal", "River", "Forest", "Wood", "Hill", "Mount", "Valley", "Meadow", "Field", "Farm", "Garden", "Orchard", "Vineyard", "Grange", "Lodge", "Court", "Hall", "House", "Cottage", "Barn", "Mill", "Windmill", "Watermill", "Forge", "Quarry", "Mine", "Pit", "Well", "Spring", "Brook", "Stream", "Lake", "Pond", "Pool", "Reservoir", "Bay", "Cove", "Beach", "Cliff", "Rock", "Stone", "Granite", "Marble", "Slate", "Clay", "Sand", "Gravel", "Chalk", "Flint", "Coal", "Iron", "Steel", "Copper", "Silver", "Gold", "Tin", "Lead", "Zinc", "Brass", "Bronze", "Pewter", "Nickel", "Cobalt", "Chromium", "Titanium", "Aluminium", "Silicon", "Carbon", "Oxygen", "Hydrogen", "Nitrogen", "Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon"]
street_suffixes = ["Street", "Road", "Lane", "Avenue", "Drive", "Boulevard", "Way", "Place", "Square", "Court", "Terrace", "Close", "Crescent", "Gardens", "Grove", "Mews", "Alley", "Walk", "Path", "Trail", "Hill", "Rise", "View", "Heights", "Park", "Green", "Meadow", "Field", "Common", "Heath", "Moor", "Down", "Fell", "Pike", "Tor", "Crag", "Cliff", "Ridge", "Edge", "Top", "Bottom", "Side", "End", "Corner", "Junction", "Cross", "Gate", "Bridge", "Ford", "Ferry", "Wharf", "Quay", "Dock", "Harbor", "Port", "Bay", "Cove", "Beach", "Shore", "Bank", "Strand", "Esplanade", "Parade", "Promenade", "Embankment", "Causeway", "Viaduct", "Tunnel", "Passage", "Arcade", "Gallery", "Mall", "Market", "Bazaar", "Fair", "Exchange", "Mart", "Emporium", "Center", "Circle", "Oval", "Triangle", "Pentagon", "Hexagon", "Octagon", "Circus", "Ring", "Loop", "Bend", "Curve", "Turn", "Twist", "Spiral", "Coil", "Helix", "Maze", "Labyrinth"]
# Facility descriptions by category
category_descriptions = {
1: [ # Recycling Bins
"Public recycling point for paper, glass, plastic, and metal",
"Community recycling station with separate bins for different materials",
"Recycling center with facilities for household waste separation",
"Public access recycling bins for common household recyclables",
"Multi-material recycling point with clear instructions for proper sorting"
],
2: [ # e-Scooters
"Dockless e-scooter rental station with multiple vehicles available",
"E-scooter parking and charging zone for public use",
"Designated e-scooter pickup and drop-off point",
"E-scooter sharing station with app-based rental system",
"Electric scooter hub with maintenance and charging facilities"
],
3: [ # Bike Share Stations
"Public bicycle sharing station with multiple bikes available",
"Bike rental hub with secure docking stations",
"Community bike share point with regular and electric bicycles",
"Cycle hire station with self-service rental system",
"Bike sharing facility with maintenance and repair services"
],
4: [ # Public EV Charging Stations
"Electric vehicle charging point with multiple connectors",
"Fast-charging station for electric vehicles",
"Public EV charging facility with covered waiting area",
"Multi-vehicle electric charging hub with different power options",
"EV charging station with renewable energy source"
],
5: [ # Battery Recycling Points
"Dedicated collection point for used batteries of all sizes",
"Battery recycling bin with separate compartments for different types",
"Safe disposal facility for household and small electronics batteries",
"Battery collection point with educational information about recycling",
"Secure battery recycling station to prevent environmental contamination"
],
6: [ # Community Compost Bins
"Neighborhood composting facility for food and garden waste",
"Community compost bins with educational signage",
"Public composting station with separate sections for different stages",
"Shared compost facility managed by local volunteers",
"Urban composting hub turning food waste into valuable soil"
],
7: [ # Solar-Powered Benches
"Solar bench with USB charging ports and WiFi connectivity",
"Public seating with integrated solar panels and device charging",
"Smart bench powered by solar energy with digital information display",
"Solar-powered rest area with phone charging capabilities",
"Eco-friendly bench with solar panels and LED lighting"
],
8: [ # Green Roofs
"Building with extensive green roof system visible from public areas",
"Accessible green roof garden with native plant species",
"Public building showcasing sustainable rooftop vegetation",
"Green roof installation with educational tours available",
"Biodiverse roof garden with insect habitats and rainwater collection"
],
9: [ # Public Water Refill Stations
"Free water refill station to reduce plastic bottle usage",
"Public drinking fountain with bottle filling capability",
"Water refill point with filtered water options",
"Accessible water station encouraging reusable bottles",
"Community water dispenser with usage counter display"
],
10: [ # Waste Oil Collection Points
"Cooking oil recycling point for residential use",
"Used oil collection facility with secure containers",
"Waste oil drop-off point for conversion to biodiesel",
"Community oil recycling station with spill prevention measures",
"Cooking oil collection facility with educational information"
],
11: [ # Book Swap Stations
"Community book exchange point with weatherproof shelving",
"Public book sharing library in repurposed phone box",
"Free book swap station encouraging reading and reuse",
"Neighborhood book exchange with rotating collection",
"Little free library with take-one-leave-one system"
],
12: [ # Pollinator Gardens
"Public garden designed to support bees and butterflies",
"Pollinator-friendly planting area with native flowering species",
"Community garden dedicated to supporting local insect populations",
"Bee-friendly garden with educational signage about pollinators",
"Urban wildflower meadow supporting biodiversity"
],
13: [ # E-Waste Collection Bins
"Secure collection point for electronic waste and small appliances",
"E-waste recycling bin for phones, computers, and small electronics",
"Electronic waste drop-off point with data security assurance",
"Community e-waste collection facility with regular collection schedule",
"Dedicated bin for responsible disposal of electronic items"
],
14: [ # Clothing Donation Bins
"Textile recycling point for clothes and household fabrics",
"Clothing donation bin supporting local charities",
"Secure collection point for reusable clothing and textiles",
"Community clothing recycling bin with regular collection",
"Textile donation point preventing landfill waste"
],
15: [ # Community Tool Libraries
"Tool lending library for community use and sharing",
"Shared equipment facility reducing need for individual ownership",
"Community resource center for borrowing tools and equipment",
"Tool sharing hub with membership system and workshops",
"Public tool library with wide range of equipment available"
],
16: [ # Urban Farms
"Community-run urban farm providing local produce",
"City farming project with volunteer opportunities",
"Urban agriculture site with educational programs",
"Local food growing initiative in repurposed urban space",
"Community garden with vegetable plots and fruit trees"
],
17: [ # Rainwater Harvesting Systems
"Public demonstration of rainwater collection for irrigation",
"Rainwater harvesting system with educational displays",
"Community rainwater collection facility for shared gardens",
"Visible rainwater storage and filtration system",
"Urban water conservation project with storage tanks"
]
}
# Status comments by category
status_comments = {
1: [ # Recycling Bins
"Recently emptied and cleaned",
"Some bins are nearly full",
"All bins in good condition",
"Paper bin is currently full",
"New signage installed to improve sorting"
],
2: [ # e-Scooters
"All scooters fully charged",
"Three scooters currently available",
"Maintenance scheduled for next week",
"New scooters added to this location",
"High usage area, scooters frequently unavailable"
],
3: [ # Bike Share Stations
"All docking stations operational",
"Five bikes currently available",
"Some bikes need maintenance",
"New electric bikes added",
"Popular station with high turnover"
],
4: [ # Public EV Charging Stations
"All charging points operational",
"Fast charger currently under repair",
"Peak usage during business hours",
"New charging point added last month",
"Payment system recently upgraded"
],
5: [ # Battery Recycling Points
"Collection bin recently emptied",
"Secure container in good condition",
"New signage explaining battery types",
"High usage from local businesses",
"Additional capacity added"
],
6: [ # Community Compost Bins
"Compost ready for collection",
"Needs more brown material",
"Recently turned and aerated",
"New bins added to increase capacity",
"Volunteer day scheduled for maintenance"
],
7: [ # Solar-Powered Benches
"All charging ports working",
"Solar panels recently cleaned",
"WiFi currently unavailable",
"LED lights need replacement",
"High usage during lunch hours"
],
8: [ # Green Roofs
"Plants thriving after recent rain",
"Maintenance scheduled next month",
"New species added to increase biodiversity",
"Irrigation system working well",
"Open for public tours on weekends"
],
9: [ # Public Water Refill Stations
"Water quality tested weekly",
"Fountain cleaned daily",
"Bottle filler counter shows high usage",
"New filter installed recently",
"Popular during summer months"
],
10: [ # Waste Oil Collection Points
"Container recently emptied",
"Secure lid in good condition",
"New funnel system installed",
"Collection schedule posted",
"Area kept clean and tidy"
],
11: [ # Book Swap Stations
"Good selection currently available",
"Children's books needed",
"Recently reorganized by volunteers",
"Weatherproof cover working well",
"High turnover of popular titles"
],
12: [ # Pollinator Gardens
"Plants in full bloom",
"Many bees and butterflies observed",
"New native species planted",
"Volunteer day for maintenance scheduled",
"Educational tours available"
],
13: [ # E-Waste Collection Bins
"Bin recently emptied",
"Secure deposit system working",
"Collection schedule posted",
"New items accepted now include small appliances",
"Data destruction guaranteed"
],
14: [ # Clothing Donation Bins
"Bin recently emptied",
"Clean and well-maintained",
"High quality donations appreciated",
"Winter clothing especially needed",
"Please bag items before donating"
],
15: [ # Community Tool Libraries
"New inventory system implemented",
"Popular tools often unavailable on weekends",
"Tool maintenance workshop scheduled",
"New donations recently added to collection",
"Extended hours during summer"
],
16: [ # Urban Farms
"Seasonal produce currently available",
"Volunteer opportunities posted",
"Educational workshops on weekends",
"New growing area being developed",
"Composting system recently expanded"
],
17: [ # Rainwater Harvesting Systems
"System working efficiently after recent rainfall",
"Water quality monitoring in place",
"Educational tours available by appointment",
"System capacity recently expanded",
"Used for irrigation of nearby community garden"
]
}
# Generate a realistic UK postcode based on area code
def generate_postcode(area_code):
# Format: Area + District + Space + Sector + Unit
# e.g., M1 1AA or SW1A 1AA
district = random.randint(1, 99)
sector = random.randint(1, 9)
unit = ''.join(random.choices('ABCDEFGHJKLMNPQRSTUVWXYZ', k=2)) # Excluding I and O as they're not used
if len(area_code) == 1:
return f"{area_code}{district} {sector}{unit}"
else:
return f"{area_code}{district} {sector}{unit}"
# Generate a realistic street name
def generate_street_name():
prefix = random.choice(street_prefixes)
suffix = random.choice(street_suffixes)
return f"{prefix} {suffix}"
# Generate a realistic house number
def generate_house_number():
# 80% chance of a simple number, 20% chance of a letter suffix or unit
if random.random() < 0.8:
return str(random.randint(1, 200))
else:
options = [
f"{random.randint(1, 200)}{random.choice('ABCDEFG')}", # e.g., 42A
f"Unit {random.randint(1, 20)}",
f"Flat {random.randint(1, 50)}",
f"Suite {random.randint(1, 10)}"
]
return random.choice(options)
# Add small random variation to coordinates to avoid facilities at exact same location
def vary_coordinates(lat, lng):
# Add variation of up to ~500 meters
lat_variation = random.uniform(-0.004, 0.004)
lng_variation = random.uniform(-0.006, 0.006)
return lat + lat_variation, lng + lng_variation
# Generate facility title based on category and location
def generate_title(category_name, location_name, street_name):
templates = [
f"{location_name} {category_name}",
f"{category_name} at {street_name}",
f"{street_name} {category_name}",
f"Community {category_name} {location_name}",
f"{location_name} Central {category_name}",
f"{location_name} {street_name} {category_name}"
]
return random.choice(templates)
# Create a log file to track progress
log_file = open("facility_generation_log.txt", "w")
log_file.write(f"Starting facility generation at {datetime.now()}\n")
log_file.write(f"Target: 1000 new facilities\n\n")
# Create a CSV file to store all generated facilities for reference
csv_file = open("generated_facilities.csv", "w", newline='')
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["ID", "Title", "Category", "Description", "Address", "Postcode", "Latitude", "Longitude", "Contributor"])
# Prepare for batch insertion to improve performance
facilities_to_insert = []
status_comments_to_insert = []
# Track unique titles to avoid duplicates
existing_titles = set()
cursor.execute("SELECT title FROM ecoFacilities")
for row in cursor.fetchall():
existing_titles.add(row[0])
# Generate 1000 facilities
num_facilities = 1000
facilities_created = 0
log_file.write("Generating facilities...\n")
while facilities_created < num_facilities:
# Select a random location
location = random.choice(uk_locations)
location_name, county, base_lat, base_lng, postcode_area = location
# Generate 5-25 facilities per location to create clusters
facilities_per_location = min(random.randint(5, 25), num_facilities - facilities_created)
for _ in range(facilities_per_location):
# Select a random category
category_id = random.choice(list(categories.keys()))
category_name = categories[category_id]
# Generate address components
street_name = generate_street_name()
house_number = generate_house_number()
lat, lng = vary_coordinates(base_lat, base_lng)
postcode = generate_postcode(postcode_area)
# Generate title
title_base = generate_title(category_name, location_name, street_name)
title = title_base
# Ensure title is unique by adding a suffix if needed
suffix = 2
while title in existing_titles:
title = f"{title_base} {suffix}"
suffix += 1
existing_titles.add(title)
# Select description
description = random.choice(category_descriptions[category_id])
# Select contributor
contributor_id = random.choice(user_ids)
# Add to batch for insertion
facilities_to_insert.append((
title,
category_id,
description,
house_number,
street_name,
county,
location_name,
postcode,
lng,
lat,
contributor_id
))
# Log progress periodically
facilities_created += 1
if facilities_created % 100 == 0:
log_message = f"Generated {facilities_created} facilities so far..."
print(log_message)
log_file.write(log_message + "\n")
if facilities_created >= num_facilities:
break
# Insert facilities in batches for better performance
log_file.write("\nInserting facilities into database...\n")
print("Inserting facilities into database...")
batch_size = 50
for i in range(0, len(facilities_to_insert), batch_size):
batch = facilities_to_insert[i:i+batch_size]
cursor.executemany("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", batch)
# Get the IDs of the inserted facilities
cursor.execute("SELECT last_insert_rowid()")
last_id = cursor.fetchone()[0]
first_id_in_batch = last_id - len(batch) + 1
# Generate status comments for each facility
for j, facility in enumerate(batch):
facility_id = first_id_in_batch + j
category_id = facility[1] # Category ID is the second element
# Write to CSV for reference
csv_writer.writerow([
facility_id,
facility[0], # title
categories[category_id], # category name
facility[2], # description
f"{facility[3]} {facility[4]}, {facility[6]}, {facility[5]}", # address
facility[7], # postcode
facility[9], # lat
facility[8], # lng
facility[10] # contributor
])
# Decide how many status comments to add (0-3)
num_comments = random.choices([0, 1, 2, 3], weights=[30, 40, 20, 10])[0]
if num_comments > 0:
# Get relevant comments for this category
relevant_comments = status_comments.get(category_id, status_comments[1]) # Default to recycling bin comments
# Select random comments without repetition
selected_comments = random.sample(relevant_comments, min(num_comments, len(relevant_comments)))
# Add to batch for insertion
for comment in selected_comments:
status_comments_to_insert.append((facility_id, comment))
# Commit after each batch
conn.commit()
log_message = f"Inserted batch {i//batch_size + 1}/{(len(facilities_to_insert)-1)//batch_size + 1}"
print(log_message)
log_file.write(log_message + "\n")
# Insert status comments in batches
if status_comments_to_insert:
log_file.write("\nInserting status comments...\n")
print("Inserting status comments...")
for i in range(0, len(status_comments_to_insert), batch_size):
batch = status_comments_to_insert[i:i+batch_size]
cursor.executemany("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", batch)
conn.commit()
log_message = f"Inserted comment batch {i//batch_size + 1}/{(len(status_comments_to_insert)-1)//batch_size + 1}"
print(log_message)
log_file.write(log_message + "\n")
# Get final counts
cursor.execute("SELECT COUNT(*) FROM ecoFacilities")
total_facilities = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM ecoFacilityStatus")
total_comments = cursor.fetchone()[0]
# Log completion
completion_message = f"\nGeneration complete at {datetime.now()}"
print(completion_message)
log_file.write(completion_message + "\n")
summary = f"Total facilities in database: {total_facilities}\n"
summary += f"Total status comments in database: {total_comments}\n"
summary += f"Generated facilities saved to generated_facilities.csv for reference"
print(summary)
log_file.write(summary)
# Close connections
log_file.close()
csv_file.close()
conn.commit()
conn.close()
print("\nSuccessfully added 1000 new ecological facilities to the database.")
print("A detailed log and CSV export have been created for reference.")

View File

@@ -0,0 +1,79 @@
<?php
// Connect to the SQLite database
$db = new PDO('sqlite:Databases/ecobuddy.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// List of real first names for usernames
$firstNames = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua',
'Kenneth', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward', 'Jason', 'Jeffrey', 'Ryan',
'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon',
'Benjamin', 'Samuel', 'Gregory', 'Alexander', 'Frank', 'Patrick', 'Raymond', 'Jack', 'Dennis', 'Jerry',
'Tyler', 'Aaron', 'Jose', 'Adam', 'Nathan', 'Henry', 'Douglas', 'Zachary', 'Peter', 'Kyle',
'Ethan', 'Walter', 'Noah', 'Jeremy', 'Christian', 'Keith', 'Roger', 'Terry', 'Gerald', 'Harold',
'Sean', 'Austin', 'Carl', 'Arthur', 'Lawrence', 'Dylan', 'Jesse', 'Jordan', 'Bryan', 'Billy',
'Joe', 'Bruce', 'Gabriel', 'Logan', 'Albert', 'Willie', 'Alan', 'Juan', 'Wayne', 'Elijah',
'Randy', 'Roy', 'Vincent', 'Ralph', 'Eugene', 'Russell', 'Bobby', 'Mason', 'Philip', 'Louis'
];
// List of common words for password generation
$words = [
'Apple', 'Banana', 'Cherry', 'Dragon', 'Eagle', 'Forest', 'Garden', 'Harbor', 'Island', 'Jungle',
'Kingdom', 'Lemon', 'Mountain', 'Nature', 'Ocean', 'Planet', 'Queen', 'River', 'Summer', 'Tiger',
'Universe', 'Volcano', 'Winter', 'Yellow', 'Zebra', 'Castle', 'Diamond', 'Emerald', 'Flower', 'Galaxy',
'Horizon', 'Iceberg', 'Journey', 'Knight', 'Legend', 'Meadow', 'Nebula', 'Oasis', 'Palace', 'Quasar',
'Rainbow', 'Sapphire', 'Thunder', 'Unicorn', 'Victory', 'Whisper', 'Xylophone', 'Yacht', 'Zephyr', 'Autumn',
'Breeze', 'Cascade', 'Dolphin', 'Eclipse', 'Falcon', 'Glacier', 'Harmony', 'Infinity', 'Jasmine', 'Kaleidoscope',
'Lighthouse', 'Mirage', 'Nightfall', 'Orchard', 'Phoenix', 'Quicksilver', 'Radiance', 'Serenity', 'Twilight', 'Umbrella'
];
// Check if we already have users with these names
$existingUsers = [];
$stmt = $db->query("SELECT username FROM ecoUser");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$existingUsers[] = $row['username'];
}
// Prepare the SQL statement for inserting users
$insertStmt = $db->prepare("INSERT INTO ecoUser (username, password, userType) VALUES (?, ?, ?)");
// Create a file to store usernames and passwords
$file = fopen("user_credentials.txt", "w");
fwrite($file, "Username,Password\n");
// Generate 50 users
$usersAdded = 0;
$usedNames = [];
shuffle($firstNames);
foreach ($firstNames as $name) {
if ($usersAdded >= 50) break;
// Skip if the name is already in the database
if (in_array($name, $existingUsers) || in_array($name, $usedNames)) {
continue;
}
// Generate password in format (Word)(Word)(Word)(Digit)
$password = $words[array_rand($words)] . $words[array_rand($words)] . $words[array_rand($words)] . rand(0, 9);
// Hash the password using SHA-256
$hashedPassword = hash('sha256', $password);
// Insert the user into the database (userType 2 is for standard users)
$insertStmt->execute([$name, $hashedPassword, 2]);
// Write the credentials to the file
fwrite($file, "$name,$password\n");
$usedNames[] = $name;
$usersAdded++;
echo "Added user: $name\n";
}
fclose($file);
echo "\nSuccessfully added $usersAdded users to the database.\n";
echo "Usernames and passwords have been saved to user_credentials.txt\n";

View File

@@ -0,0 +1,79 @@
import sqlite3
import random
import hashlib
import os
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# List of real first names for usernames
first_names = [
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Charles',
'Christopher', 'Daniel', 'Matthew', 'Anthony', 'Mark', 'Donald', 'Steven', 'Paul', 'Andrew', 'Joshua',
'Kenneth', 'Kevin', 'Brian', 'George', 'Timothy', 'Ronald', 'Edward', 'Jason', 'Jeffrey', 'Ryan',
'Jacob', 'Gary', 'Nicholas', 'Eric', 'Jonathan', 'Stephen', 'Larry', 'Justin', 'Scott', 'Brandon',
'Benjamin', 'Samuel', 'Gregory', 'Alexander', 'Frank', 'Patrick', 'Raymond', 'Jack', 'Dennis', 'Jerry',
'Tyler', 'Aaron', 'Jose', 'Adam', 'Nathan', 'Henry', 'Douglas', 'Zachary', 'Peter', 'Kyle',
'Ethan', 'Walter', 'Noah', 'Jeremy', 'Christian', 'Keith', 'Roger', 'Terry', 'Gerald', 'Harold',
'Sean', 'Austin', 'Carl', 'Arthur', 'Lawrence', 'Dylan', 'Jesse', 'Jordan', 'Bryan', 'Billy',
'Joe', 'Bruce', 'Gabriel', 'Logan', 'Albert', 'Willie', 'Alan', 'Juan', 'Wayne', 'Elijah',
'Randy', 'Roy', 'Vincent', 'Ralph', 'Eugene', 'Russell', 'Bobby', 'Mason', 'Philip', 'Louis'
]
# List of common words for password generation
words = [
'Apple', 'Banana', 'Cherry', 'Dragon', 'Eagle', 'Forest', 'Garden', 'Harbor', 'Island', 'Jungle',
'Kingdom', 'Lemon', 'Mountain', 'Nature', 'Ocean', 'Planet', 'Queen', 'River', 'Summer', 'Tiger',
'Universe', 'Volcano', 'Winter', 'Yellow', 'Zebra', 'Castle', 'Diamond', 'Emerald', 'Flower', 'Galaxy',
'Horizon', 'Iceberg', 'Journey', 'Knight', 'Legend', 'Meadow', 'Nebula', 'Oasis', 'Palace', 'Quasar',
'Rainbow', 'Sapphire', 'Thunder', 'Unicorn', 'Victory', 'Whisper', 'Xylophone', 'Yacht', 'Zephyr', 'Autumn',
'Breeze', 'Cascade', 'Dolphin', 'Eclipse', 'Falcon', 'Glacier', 'Harmony', 'Infinity', 'Jasmine', 'Kaleidoscope',
'Lighthouse', 'Mirage', 'Nightfall', 'Orchard', 'Phoenix', 'Quicksilver', 'Radiance', 'Serenity', 'Twilight', 'Umbrella'
]
# Check if we already have users with these names
cursor.execute("SELECT username FROM ecoUser")
existing_users = [row[0] for row in cursor.fetchall()]
# Create a file to store usernames and passwords
with open("user_credentials.txt", "w") as file:
file.write("Username,Password\n")
# Generate 50 users
users_added = 0
used_names = []
random.shuffle(first_names)
for name in first_names:
if users_added >= 50:
break
# Skip if the name is already in the database
if name in existing_users or name in used_names:
continue
# Generate password in format (Word)(Word)(Word)(Digit)
password = random.choice(words) + random.choice(words) + random.choice(words) + str(random.randint(0, 9))
# Hash the password using SHA-256
hashed_password = hashlib.sha256(password.encode()).hexdigest()
# Insert the user into the database (userType 2 is for standard users)
cursor.execute("INSERT INTO ecoUser (username, password, userType) VALUES (?, ?, ?)",
(name, hashed_password, 2))
# Write the credentials to the file
file.write(f"{name},{password}\n")
used_names.append(name)
users_added += 1
print(f"Added user: {name}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print(f"\nSuccessfully added {users_added} users to the database.")
print("Usernames and passwords have been saved to user_credentials.txt")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
Username,Password
Dylan,MirageAutumnUmbrella7
Lawrence,MirageVictoryKingdom5
Bryan,EmeraldOrchardLegend3
Alan,ThunderOceanDiamond2
Frank,XylophoneVictoryHarmony3
Logan,NatureSerenityXylophone5
Jerry,LighthousePhoenixYellow8
Harold,IcebergGlacierTiger8
Keith,YachtJourneyGarden4
Arthur,VictoryOrchardFlower0
Louis,FlowerNebulaNature3
Paul,LegendLegendYacht4
Aaron,RiverCascadeApple3
George,MeadowBreezePalace4
Carl,RadianceMirageQuasar2
Kenneth,IcebergPlanetHorizon8
Daniel,BreezeXylophoneGalaxy5
Ronald,PhoenixThunderZephyr2
Benjamin,NatureVolcanoNebula8
Joe,UnicornSapphireHorizon1
Larry,IslandZebraApple6
Zachary,SapphireUnicornJasmine0
Willie,UmbrellaVictoryHorizon8
Anthony,QueenZebraNebula9
Michael,TigerMountainNightfall8
Vincent,InfinityHorizonQuicksilver8
Roger,WinterXylophonePalace8
Kyle,IcebergDolphinDragon9
Henry,HarmonyKnightPalace7
Eugene,JourneyThunderPalace0
Billy,ThunderThunderQuicksilver9
Peter,OrchardJasmineVictory4
Christopher,QueenThunderAutumn5
Adam,SummerUnicornThunder7
Nathan,SerenityOrchardThunder2
Edward,MeadowGalaxyYellow6
Eric,DolphinNebulaYacht8
Brian,CastleFlowerGlacier4
Alexander,ForestSapphireZebra4
Andrew,GalaxyLemonApple0
Brandon,HarmonyTigerHarmony2
Russell,CherryZebraQuicksilver8
Jack,OrchardZephyrSapphire8
Jose,BananaJungleSerenity8
Jacob,KaleidoscopeEmeraldJasmine5
Dennis,KnightFlowerRainbow2
Donald,WhisperQuicksilverCastle1
William,ApplePalaceSummer6
Patrick,CastleInfinityPhoenix9
Timothy,YellowEagleSummer0

112
Models/AuthExample.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
/**
* Example controller showing how to use the simplified authentication
*
* This file demonstrates how to use the User::checkAuth() and User::checkAdmin()
* methods to protect routes without using middleware.
*/
require_once('Models/User.php');
/**
* Example of a protected endpoint that requires authentication
*/
function protectedEndpoint() {
// Check if user is authenticated
$auth = User::checkAuth();
if (!$auth) {
// The checkAuth method already sent the error response
return;
}
// User is authenticated, proceed with the endpoint logic
$response = [
'status' => 'success',
'message' => 'You are authenticated',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username']
]
];
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of an admin-only endpoint
*/
function adminEndpoint() {
// Check if user is an admin
$auth = User::checkAdmin();
if (!$auth) {
// The checkAdmin method already sent the error response
return;
}
// User is an admin, proceed with the admin-only logic
$response = [
'status' => 'success',
'message' => 'You have admin access',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username']
]
];
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of a public endpoint that doesn't require authentication
* but can still use authentication data if available
*/
function publicEndpoint() {
// Check if user is authenticated, but don't require it
$auth = User::checkAuth(false);
$response = [
'status' => 'success',
'message' => 'This is a public endpoint'
];
// Add user info if authenticated
if ($auth) {
$response['user'] = [
'id' => $auth['uid'],
'username' => $auth['username']
];
} else {
$response['user'] = 'Guest';
}
// Send response
header('Content-Type: application/json');
echo json_encode($response);
}
/**
* Example of how to use these functions in a simple router
*/
function handleRequest() {
$route = $_GET['route'] ?? 'public';
switch ($route) {
case 'protected':
protectedEndpoint();
break;
case 'admin':
adminEndpoint();
break;
case 'public':
default:
publicEndpoint();
break;
}
}
// Call the router function
handleRequest();

199
Models/AuthService.php Normal file
View File

@@ -0,0 +1,199 @@
<?php
require_once('UserDataSet.php');
/**
* Authentication service for handling JWT-based authentication
*/
class AuthService {
private string $secretKey;
private int $tokenExpiry;
/**
* Initialises the authentication service
* Loads configuration from environment variables
* @throws Exception if OpenSSL extension is not loaded
*/
public function __construct() {
// Load environment variables from .env file
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Skip comments
if (strpos($line, '#') === 0) continue;
// Parse environment variable
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!empty($name)) {
putenv(sprintf('%s=%s', $name, $value));
}
}
}
// Set configuration from environment variables with defaults
$this->secretKey = getenv('JWT_SECRET_KEY') ?: 'your-256-bit-secret';
$this->tokenExpiry = (int)(getenv('JWT_TOKEN_EXPIRY') ?: 3600);
// Verify OpenSSL extension is available
if (!extension_loaded('openssl')) {
throw new Exception('OpenSSL extension is required for JWT');
}
}
/**
* Generates a JWT token for a user
* @param array $userData User information to include in token
* @return string The generated JWT token
*/
public function generateToken(array $userData): string {
$issuedAt = time();
$expire = $issuedAt + $this->tokenExpiry;
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'uid' => $userData['id'],
'username' => $userData['username'],
'accessLevel' => $userData['userType']
];
return $this->encodeJWT($payload);
}
/**
* Validates a JWT token
* @param string $token The JWT token to validate
* @return array|null The decoded payload if valid, null otherwise
*/
public function validateToken(string $token): ?array {
try {
$payload = $this->decodeJWT($token);
// Check if token is expired
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time()) {
return null;
}
return $payload;
} catch (Exception $e) {
return null;
}
}
/**
* Encodes data into a JWT token
* @param array $payload The data to encode
* @return string The encoded JWT token
*/
private function encodeJWT(array $payload): string {
// Create and encode header
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
$header = $this->base64UrlEncode($header);
// Create and encode payload
$payload = json_encode($payload);
$payload = $this->base64UrlEncode($payload);
// Create and encode signature
$signature = hash_hmac('sha256', "$header.$payload", $this->secretKey, true);
$signature = $this->base64UrlEncode($signature);
return "$header.$payload.$signature";
}
/**
* Decodes a JWT token
* @param string $token The JWT token to decode
* @return array|null The decoded payload if valid, null otherwise
*/
private function decodeJWT(string $token): ?array {
// Split token into components
$parts = explode('.', $token);
if (count($parts) !== 3) {
return null;
}
[$header, $payload, $signature] = $parts;
// Verify signature
$validSignature = $this->base64UrlEncode(
hash_hmac('sha256', "$header.$payload", $this->secretKey, true)
);
if ($signature !== $validSignature) {
return null;
}
// Decode and return payload
return json_decode($this->base64UrlDecode($payload), true);
}
/**
* Encodes data using base64url encoding
* @param string $data The data to encode
* @return string The encoded data
*/
private function base64UrlEncode(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Decodes base64url encoded data
* @param string $data The data to decode
* @return string The decoded data
*/
private function base64UrlDecode(string $data): string {
return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', 3 - (3 + strlen($data)) % 4));
}
/**
* Generates a refresh token for a user
* @param array $userData User information to include in token
* @return string The generated refresh token
*/
public function generateRefreshToken(array $userData): string {
$issuedAt = time();
$expire = $issuedAt + ($this->tokenExpiry * 24); // Refresh token lasts 24 times longer than access token
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'uid' => $userData['id'],
'username' => $userData['username'],
'type' => 'refresh'
];
return $this->encodeJWT($payload);
}
/**
* Refreshes an access token using a refresh token
* @param string $refreshToken The refresh token
* @return string|null The new access token if valid, null otherwise
*/
public function refreshToken(string $refreshToken): ?string {
try {
$payload = $this->decodeJWT($refreshToken);
// Check if token is expired or not a refresh token
if ($payload === null || !isset($payload['exp']) || $payload['exp'] < time() ||
!isset($payload['type']) || $payload['type'] !== 'refresh') {
return null;
}
// Generate a new access token
$userData = [
'id' => $payload['uid'],
'username' => $payload['username'],
'userType' => isset($payload['accessLevel']) ? $payload['accessLevel'] : 0
];
return $this->generateToken($userData);
} catch (Exception $e) {
return null;
}
}
}

43
Models/Database.php Normal file → Executable file
View File

@@ -1,18 +1,31 @@
<?php
/**
* Database connection handler using Singleton pattern
*/
class Database {
/**
* @var Database
* @var Database|null The singleton instance
*/
protected static $_dbInstance = null;
/**
* @var PDO
* @var PDO The database connection handle
*/
protected $_dbHandle;
/**
* Gets the database connection handle
* @return PDO The database connection
*/
public function getDbConnection(): PDO
{
return $this->_dbHandle;
}
/**
* Gets the singleton instance of the Database class
* @return Database The database instance
*/
public static function getInstance(): ?Database
{
if(self::$_dbInstance == null) {
@@ -21,17 +34,37 @@ class Database {
return self::$_dbInstance;
}
/**
* Private constructor to prevent direct instantiation
* Initialises the database connection
* @throws PDOException if connection fails
*/
private function __construct() {
try {
$this->_dbHandle = new PDO("sqlite:Databases/ecobuddynew.sqlite");
// Create PDO connection with error handling
$this->_dbHandle = new PDO("sqlite:Databases/ecobuddy.sqlite");
// Configure PDO for better error handling and performance
$this->_dbHandle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$this->_dbHandle->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
// SQLite3 sometimes just forgets foreign keys exist i guess (https://stackoverflow.com/questions/15301643/sqlite3-forgets-to-use-foreign-keys)
$this->_dbHandle->exec('PRAGMA foreign_keys = ON;');
// Set transaction timeout to 5 seconds, just stops the app from hanging when the db is busy
$this->_dbHandle->exec('PRAGMA busy_timeout = 5000;');
}
catch (PDOException $e) {
echo $e->getMessage();
// Log the error and rethrow
error_log("Database connection error: " . $e->getMessage());
throw $e;
}
}
/**
* Destructor to clean up database connection
*/
public function __destruct() {
$this->_dbHandle = null; // destroys the PDO handle when no longer needed
$this->_dbHandle = null;
}
}

158
Models/FacilityData.php Normal file → Executable file
View File

@@ -1,7 +1,52 @@
<?php
/**
* Represents a facility in the EcoBuddy system
*
* This class serves as a data model for facilities, encapsulating all
* the properties and behaviours of a single facility. It follows the
* Data Transfer Object (DTO) pattern that I learned about in my
* software architecture module.
*
* Each facility has location data, descriptive information, and metadata
* about who contributed it. This class provides a clean interface for
* accessing this data throughout the application.
*/
class FacilityData {
protected $_id, $_title, $_category, $_status, $_description, $_houseNumber, $_streetName, $_county, $_town, $_postcode, $_lng, $_lat, $_contributor;
/**
* Facility properties
*
* @var int $_id - Unique identifier for the facility
* @var string $_title - Name of the facility
* @var string $_category - Category/type of the facility
* @var string $_status - Current status of the facility
* @var string $_description - Detailed description of the facility
* @var string $_houseNumber - Building number or name
* @var string $_streetName - Street name
* @var string $_county - County
* @var string $_town - Town or city
* @var string $_postcode - Postal code
* @var float $_lng - Longitude coordinate
* @var float $_lat - Latitude coordinate
* @var string $_contributor - Username of the person who added the facility
*/
protected $_id;
protected $_title;
protected $_category;
protected $_status;
protected $_description;
protected $_houseNumber;
protected $_streetName;
protected $_county;
protected $_town;
protected $_postcode;
protected $_lng;
protected $_lat;
protected $_contributor;
/**
* Initialises a new facility with data from the database
* @param array $dbRow Database row containing facility data
*/
public function __construct($dbRow) {
$this->_id = $dbRow['id'];
$this->_title = $dbRow['title'];
@@ -18,43 +63,154 @@ class FacilityData {
$this->_contributor = $dbRow['contributor'];
}
/**
* Gets the facility's unique identifier
*
* This ID is used throughout the application to reference this specific
* facility, particularly in database operations and API requests.
*
* @return int The facility ID
*/
public function getId() {
return $this->_id;
}
/**
* Gets the facility's title
*
* The title is the primary name or label for the facility that
* is displayed to users in the interface.
*
* @return string The facility title
*/
public function getTitle() {
return $this->_title;
}
/**
* Gets the facility's category
*
* The category helps classify facilities by type, such as
* recycling centre, community garden, etc.
*
* @return string The facility category
*/
public function getCategory() {
return $this->_category;
}
/**
* Gets the facility's current status
*
* The status indicates whether the facility is operational,
* under maintenance, closed, etc.
*
* @return string The facility status
*/
public function getStatus() {
return $this->_status;
}
/**
* Gets the facility's description
*
* The description provides detailed information about the facility,
* its purpose, services offered, etc.
*
* @return string The facility description
*/
public function getDescription() {
return $this->_description;
}
/**
* Gets the facility's house/building number
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The house/building number
*/
public function getHouseNumber() {
return $this->_houseNumber;
}
/**
* Gets the facility's street name
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The street name
*/
public function getStreetName() {
return $this->_streetName;
}
/**
* Gets the facility's county
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The county
*/
public function getCounty() {
return $this->_county;
}
/**
* Gets the facility's town or city
*
* This is part of the facility's address and helps locate it physically.
*
* @return string The town or city
*/
public function getTown() {
return $this->_town;
}
/**
* Gets the facility's postcode
*
* This is part of the facility's address and helps locate it physically.
* It's also useful for searching facilities by location.
*
* @return string The postcode
*/
public function getPostcode() {
return $this->_postcode;
}
/**
* Gets the facility's longitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The longitude coordinate
*/
public function getLng() {
return $this->_lng;
}
/**
* Gets the facility's latitude coordinate
*
* This is used for displaying the facility on a map and
* for calculating distances between facilities.
*
* @return float The latitude coordinate
*/
public function getLat() {
return $this->_lat;
}
/**
* Gets the username of the facility's contributor
*
* This tracks who added the facility to the system,
* which is useful for auditing and attribution.
*
* @return string The contributor's username
*/
public function getContributor() {
return $this->_contributor;
}

586
Models/FacilityDataSet.php Normal file → Executable file
View File

@@ -12,76 +12,6 @@ class FacilityDataSet
$this->_dbHandle = $this->_dbInstance->getDbConnection();
}
/**
* @param $data
* @return bool
* Broken last minute, dont have time to fix.
* add / update facility to database from array of columns
*/
public function addFacility($data): bool
{
$userQuery = "
SELECT ecoUser.id FROM ecoUser
WHERE ecoUser.username = :contributor;
";
$catQuery = "
SELECT ecoCategories.id FROM ecoCategories
WHERE ecoCategories.name = :category;
";
$sqlQuery = "
INSERT OR REPLACE INTO ecoFacilities
(id,
title,
category,
description,
houseNumber,
streetName,
county,
town,
postcode,
lng,
lat,
contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, -1, -1, ?)
;";
// gets contributor name
$stmt = $this->_dbHandle->prepare($userQuery);
$stmt->bindParam(':contributor', $data->contributor, PDO::PARAM_STR);
$stmt = $this->_dbHandle->prepare($userQuery);
$stmt->execute();
$data['contributor'] = (int)$stmt->fetch(PDO::FETCH_ASSOC);
// gets category ID
$stmt = $this->_dbHandle->prepare($catQuery);
$stmt->bindParam(':category', $data->category, PDO::PARAM_STR);
$stmt = $this->_dbHandle->prepare($catQuery);
$stmt->execute();
$data['category'] = (int)$stmt->fetch(PDO::FETCH_ASSOC);
// run main query and bind updated parameters
$stmt = $this->_dbHandle->prepare($sqlQuery);
// Ensures only one value is returned per column name
$stmt->setFetchMode(\PDO::FETCH_ASSOC);
if (isset($data['id'])) {
$stmt->bindParam(1, $data['id']);
}
$stmt->bindParam(2, $data['title'], PDO::PARAM_STR);
$stmt->bindParam(3, $data['category'], PDO::PARAM_INT);
$stmt->bindParam(4, $data['description'], PDO::PARAM_STR);
$stmt->bindParam(5, $data['houseNumber'], PDO::PARAM_STR);
$stmt->bindParam(6, $data['streetName'], PDO::PARAM_STR);
$stmt->bindParam(7, $data['county'], PDO::PARAM_STR);
$stmt->bindParam(8, $data['town'], PDO::PARAM_STR);
$stmt->bindParam(9, $data['postcode'], PDO::PARAM_STR);
$stmt->bindParam(10, $data['contributor'], PDO::PARAM_INT);
$stmt->execute();
// var_dump($stmt);
// var_dump($this->_dbHandle->errorInfo());
return !($stmt->rowCount());
}
/**
* @param $id
* @return bool
@@ -89,114 +19,444 @@ class FacilityDataSet
*/
public function deleteFacility($id): bool
{
$sqlQuery = "DELETE FROM ecoFacilities WHERE ecoFacilities.id = :id;";
$stmt = $this->_dbHandle->prepare($sqlQuery);
$stmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$stmt->execute();
var_dump($stmt);
echo $stmt->rowCount();
return !($stmt->rowCount() == 0);
try {
// Start transaction
$this->_dbHandle->beginTransaction();
// Delete related status records first
$statusQuery = "DELETE FROM ecoFacilityStatus WHERE facilityid = :id;";
$statusStmt = $this->_dbHandle->prepare($statusQuery);
$statusStmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$statusStmt->execute();
// Delete the facility
$facilityQuery = "DELETE FROM ecoFacilities WHERE id = :id;";
$facilityStmt = $this->_dbHandle->prepare($facilityQuery);
$facilityStmt->bindValue(':id', (int)$id, \PDO::PARAM_INT);
$facilityStmt->execute();
// Commit transaction
$this->_dbHandle->commit();
return $facilityStmt->rowCount() > 0;
} catch (PDOException $e) {
// Rollback on error
$this->_dbHandle->rollBack();
error_log("Error deleting facility: " . $e->getMessage());
return false;
}
}
/**
* @return array|false Returns array of facilities or false on error
* Fetch all facility records with related data
*/
public function fetchAll(): array|false
{
try {
error_log('Starting fetchAll...');
$query = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
COALESCE(GROUP_CONCAT(ecoFacilityStatus.statusComment, '; '), '') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
COALESCE(ecoUser.username, 'Unknown') AS contributor
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username
ORDER BY ecoFacilities.id ASC;
";
error_log('Preparing query...');
$dataStmt = $this->_dbHandle->prepare($query);
error_log('Executing query...');
$dataStmt->execute();
error_log('Fetching results...');
$results = $dataStmt->fetchAll(PDO::FETCH_ASSOC);
if ($results === false) {
error_log('Query returned false');
return false;
}
error_log('Query successful. Row count: ' . count($results));
return $results;
} catch (PDOException $e) {
error_log("Database error in fetchAll: " . $e->getMessage());
error_log("SQL State: " . $e->getCode());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
} catch (Exception $e) {
error_log("General error in fetchAll: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* @param $filterArray
* @param $sortArray
* @return array
* Fetch all records depending on filters, and sort by defined column
* Creates a new facility in the database
* @param array $data Facility data
* @return array|false The created facility data or false on failure
*/
public function fetchAll($filterArray, $sortArray): array
public function createFacility($data)
{
// Define columns for filtering and sorting
$filterColumns = [
0 => 'ecoFacilityStatus.statusComment',
1 => 'ecoFacilities.title',
2 => 'ecoCategories.name',
3 => 'ecoFacilities.description',
4 => 'ecoFacilities.streetName',
5 => 'ecoFacilities.county',
6 => 'ecoFacilities.town',
7 => 'ecoFacilities.postcode',
8 => 'ecoUser.username'
];
try {
$this->_dbHandle->beginTransaction();
$sortColumns = [
0 => 'ecoFacilityStatus.statusComment',
1 => 'ecoFacilities.title',
2 => 'ecoCategories.name',
3 => 'ecoFacilities.description',
4 => 'ecoFacilities.streetName',
5 => 'ecoFacilities.county',
6 => 'ecoFacilities.town',
7 => 'ecoFacilities.postcode',
8 => 'ecoUser.username'
];
// Validate coordinates
if (!is_numeric($data['lng']) || !is_numeric($data['lat']) ||
$data['lng'] < -180 || $data['lng'] > 180 ||
$data['lat'] < -90 || $data['lat'] > 90) {
throw new Exception('Invalid coordinates provided');
}
// Validate and select the filter column
$selectedFilterColumn = $filterColumns[$filterArray['category']] ?? 'ecoFacilities.title';
// Validate and select the sort column
$selectedSortColumn = $sortColumns[$sortArray['sort']] ?? 'ecoFacilities.title';
// Validate sort direction
$direction = strtolower($sortArray['dir']) === 'desc' ? 'DESC' : 'ASC';
// Base query for filtering and sorting
$baseQuery = "
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
WHERE {$selectedFilterColumn} LIKE :term
";
// Get contributor ID
$contributorId = $this->getContributorId($data['contributor']);
if (!$contributorId) {
throw new Exception('Invalid contributor name');
}
// Get category ID
$categoryId = $this->getCategoryId($data['category']);
if (!$categoryId) {
// If category doesn't exist, create it
$categoryId = $this->createCategory($data['category']);
if (!$categoryId) {
throw new Exception('Failed to create category: ' . $data['category']);
}
}
// Query to count total results
$countQuery = "SELECT COUNT(DISTINCT ecoFacilities.id) AS total {$baseQuery}";
// Insert facility
$sql = "INSERT INTO ecoFacilities (title, category, description, houseNumber,
streetName, county, town, postcode, lng, lat, contributor)
VALUES (:title, :category, :description, :houseNumber,
:streetName, :county, :town, :postcode, :longitude, :latitude, :contributor)";
$stmt = $this->_dbHandle->prepare($sql);
$params = [
':title' => $data['title'],
':category' => $categoryId,
':description' => $data['description'],
':houseNumber' => $data['houseNumber'],
':streetName' => $data['streetName'],
':county' => $data['county'],
':town' => $data['town'],
':postcode' => $data['postcode'],
':longitude' => $data['lng'],
':latitude' => $data['lat'],
':contributor' => $contributorId
];
// Query to fetch filtered and sorted results
$dataQuery = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
GROUP_CONCAT(ecoFacilityStatus.statusComment, ', ') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
ecoUser.username AS contributor
{$baseQuery}
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username
ORDER BY {$selectedSortColumn} {$direction};
";
// Surround 'term' with % to allow usage with LIKE
$filterArray['term'] = '%' . $filterArray['term'] . '%' ?? '%';
// Prepare and execute the count query
$countStmt = $this->_dbHandle->prepare($countQuery);
$countStmt->bindValue(':term', $filterArray['term'], PDO::PARAM_STR);
$countStmt->execute();
// Set total results to output of count statement
$totalResults = (int)$countStmt->fetchColumn();
error_log("Executing SQL with params: " . print_r($params, true));
if (!$stmt->execute($params)) {
throw new Exception('Failed to insert facility: ' . implode(', ', $stmt->errorInfo()));
}
// Prepare and execute the data query
$dataStmt = $this->_dbHandle->prepare($dataQuery);
$dataStmt->bindValue(':term', $filterArray['term'], PDO::PARAM_STR);
$dataStmt->execute();
$facilityId = $this->_dbHandle->lastInsertId();
$this->_dbHandle->commit();
// Fetch results into FacilityData objects
$dataSet = [];
while ($row = $dataStmt->fetch()) {
$dataSet[] = new FacilityData($row);
// Return the created facility
return $this->getFacilityById($facilityId);
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error in createFacility: " . $e->getMessage());
throw $e;
}
}
return [
'dataset' => $dataSet,
'count' => $totalResults
];
private function createCategory($categoryName)
{
try {
$sql = "INSERT INTO ecoCategories (name) VALUES (:name)";
$stmt = $this->_dbHandle->prepare($sql);
$stmt->execute([':name' => $categoryName]);
return $this->_dbHandle->lastInsertId();
} catch (Exception $e) {
error_log("Error creating category: " . $e->getMessage());
return false;
}
}
/**
* Updates an existing facility in the database
* @param int $id Facility ID
* @param array $data Updated facility data
* @return array|false The updated facility data or false on failure
*/
public function updateFacility($id, $data) {
try {
// Start transaction
$this->_dbHandle->beginTransaction();
// Validate coordinates
if (!is_numeric($data['lng']) || !is_numeric($data['lat']) ||
$data['lng'] < -180 || $data['lng'] > 180 ||
$data['lat'] < -90 || $data['lat'] > 90) {
throw new Exception('Invalid coordinates');
}
// Get Contributor ID
$query = "SELECT ecoUser.id FROM ecoUser WHERE ecoUser.username = :contributor;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':contributor', $data['contributor']);
$stmt->execute();
$contributorResult = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$contributorResult) {
throw new Exception('Invalid contributor username');
}
$contributorId = $contributorResult['id'];
// Get Category ID
$query = "SELECT ecoCategories.id FROM ecoCategories WHERE ecoCategories.name = :category;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':category', $data['category']);
$stmt->execute();
$categoryResult = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$categoryResult) {
throw new Exception('Invalid category name');
}
$categoryId = $categoryResult['id'];
// Update facility
$query = "
UPDATE ecoFacilities
SET title = :title,
category = :category,
description = :description,
houseNumber = :houseNumber,
streetName = :streetName,
county = :county,
town = :town,
postcode = :postcode,
lng = :lng,
lat = :lat,
contributor = :contributor
WHERE id = :id
";
$stmt = $this->_dbHandle->prepare($query);
$params = [
':title' => $data['title'],
':category' => $categoryId,
':description' => $data['description'],
':houseNumber' => $data['houseNumber'],
':streetName' => $data['streetName'],
':county' => $data['county'],
':town' => $data['town'],
':postcode' => $data['postcode'],
':lng' => $data['lng'],
':lat' => $data['lat'],
':contributor' => $contributorId,
':id' => $id
];
error_log("Executing update query with params: " . print_r($params, true));
if (!$stmt->execute($params)) {
throw new Exception('Failed to update facility: ' . implode(', ', $stmt->errorInfo()));
}
if ($stmt->rowCount() > 0) {
$this->_dbHandle->commit();
return $this->getFacilityById($id);
}
$this->_dbHandle->rollBack();
return false;
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error updating facility: " . $e->getMessage());
return false;
}
}
/**
* Gets a facility by its ID
* @param int $id Facility ID
* @return array|false The facility data or false if not found
*/
public function getFacilityById($id) {
try {
$query = "
SELECT DISTINCT ecoFacilities.id,
ecoFacilities.title,
COALESCE(GROUP_CONCAT(ecoFacilityStatus.statusComment, ';'), '') AS status,
ecoCategories.name AS category,
ecoFacilities.description,
ecoFacilities.houseNumber,
ecoFacilities.streetName,
ecoFacilities.county,
ecoFacilities.town,
ecoFacilities.postcode,
ecoFacilities.lng,
ecoFacilities.lat,
COALESCE(ecoUser.username, 'Unknown') AS contributor
FROM ecoFacilities
LEFT JOIN ecoCategories ON ecoCategories.id = ecoFacilities.category
LEFT JOIN ecoUser ON ecoUser.id = ecoFacilities.contributor
LEFT JOIN ecoFacilityStatus ON ecoFacilityStatus.facilityid = ecoFacilities.id
WHERE ecoFacilities.id = ?
GROUP BY ecoFacilities.id, ecoFacilities.title, ecoCategories.name,
ecoFacilities.description, ecoFacilities.streetName,
ecoFacilities.county, ecoFacilities.town, ecoFacilities.postcode,
ecoUser.username;
";
$stmt = $this->_dbHandle->prepare($query);
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Error getting facility: " . $e->getMessage());
return false;
}
}
private function getContributorId($username)
{
try {
$query = "SELECT ecoUser.id FROM ecoUser WHERE ecoUser.username = :username;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':username', $username);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['id'] : false;
} catch (Exception $e) {
error_log("Error getting contributor ID: " . $e->getMessage());
return false;
}
}
private function getCategoryId($categoryName)
{
try {
$query = "SELECT ecoCategories.id FROM ecoCategories WHERE ecoCategories.name = :name;";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':name', $categoryName);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['id'] : false;
} catch (Exception $e) {
error_log("Error getting category ID: " . $e->getMessage());
return false;
}
}
/**
* Adds a new status comment to a facility
* @param int $facilityId The ID of the facility
* @param string $statusComment The status comment to add
* @return bool True if successful, false otherwise
*/
public function addFacilityStatus($facilityId, $statusComment)
{
try {
// Log input parameters
error_log("Adding facility status - Facility ID: " . $facilityId . ", Comment: " . $statusComment);
// Start transaction
$this->_dbHandle->beginTransaction();
// Insert new status comment
$query = "INSERT INTO ecoFacilityStatus (facilityId, statusComment) VALUES (:facilityId, :statusComment)";
$stmt = $this->_dbHandle->prepare($query);
// Log the prepared statement
error_log("Prepared statement: " . $query);
// Bind values and log them
$stmt->bindValue(':facilityId', (int)$facilityId, PDO::PARAM_INT);
$stmt->bindValue(':statusComment', $statusComment);
error_log("Bound values - Facility ID: " . (int)$facilityId . ", Comment: " . $statusComment);
if (!$stmt->execute()) {
$errorInfo = $stmt->errorInfo();
error_log("SQL Error: " . print_r($errorInfo, true));
throw new Exception('Failed to insert status comment: ' . implode(', ', $errorInfo));
}
$this->_dbHandle->commit();
error_log("Successfully added facility status");
return true;
} catch (Exception $e) {
$this->_dbHandle->rollBack();
error_log("Error adding facility status: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
return false;
}
}
/**
* Gets all status comments for a facility
* @param int $facilityId The ID of the facility
* @return array Array of status comments with their IDs
*/
public function getFacilityStatuses($facilityId)
{
try {
$query = "SELECT id, statusComment FROM ecoFacilityStatus WHERE facilityId = :facilityId ORDER BY id DESC";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':facilityId', (int)$facilityId, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
error_log("Error getting facility statuses: " . $e->getMessage());
return [];
}
}
/**
* Updates an existing status comment
* @param int $statusId The ID of the status comment
* @param string $statusComment The updated status comment
* @return bool True if successful, false otherwise
*/
public function updateFacilityStatus($statusId, $statusComment)
{
try {
$query = "UPDATE ecoFacilityStatus SET statusComment = :statusComment WHERE id = :statusId";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':statusId', (int)$statusId, PDO::PARAM_INT);
$stmt->bindValue(':statusComment', $statusComment);
return $stmt->execute();
} catch (Exception $e) {
error_log("Error updating facility status: " . $e->getMessage());
return false;
}
}
/**
* Deletes a specific status comment
* @param int $statusId The ID of the status comment to delete
* @return bool True if successful, false otherwise
*/
public function deleteFacilityStatus($statusId)
{
try {
$query = "DELETE FROM ecoFacilityStatus WHERE id = :statusId";
$stmt = $this->_dbHandle->prepare($query);
$stmt->bindValue(':statusId', (int)$statusId, PDO::PARAM_INT);
return $stmt->execute();
} catch (Exception $e) {
error_log("Error deleting facility status: " . $e->getMessage());
return false;
}
}
}

0
Models/Paginator.php Normal file → Executable file
View File

191
Models/User.php Normal file → Executable file
View File

@@ -1,62 +1,133 @@
<?php
require_once('UserDataSet.php');
class User {
protected $_username, $_loggedIn, $_userId, $_accessLevel;
require_once('AuthService.php');
/**
* User class - Handles user authentication and session management
*
* This class manages user authentication using JWT tokens and provides
* methods for logging in, logging out, and checking user permissions.
* I've implemented this based on JWT authentication
*/
class User {
/**
* Class properties
* @var string $_username - The user's username
* @var bool $_loggedIn - Whether the user is currently logged in
* @var string $_userId - The user's unique ID
* @var int $_accessLevel - The user's access level (admin = 1, regular user = 2)
* @var AuthService $_authService - Service for JWT token handling
*/
protected $_username, $_loggedIn, $_userId, $_accessLevel;
protected $_authService;
/**
* Gets the current user's username
*
* @return string The username of the current user
*/
public function getUsername() {
return $this->_username;
}
/**
* Gets the current user's ID
*
* @return string The ID of the current user
*/
public function getUserId() {
return $this->_userId;
}
/**
* Open session, set field variables
* Constructor - Initialises user from JWT token if available
*
* Checks for a JWT token in the Authorization header and validates it.
* If valid, sets user properties based on the token payload.
* Also starts a session if needed for CAPTCHA verification during registration.
*/
public function __construct() {
session_start();
// Initialise default values
$this->_username = "None";
$this->_loggedIn = false;
$this->_userId = "0";
$this->_accessLevel = null;
// if user logged in, set variables.
if(isset($_SESSION['login'])) {
$this->_username = $_SESSION['login'];
$this->_userId = $_SESSION['uid'];
$this->_loggedIn = true;
$this->_accessLevel = $_SESSION['accessLevel'];
$this->_authService = new AuthService();
// Check for JWT token in Authorization header
$headers = getallheaders();
$token = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : null;
// Validate token if it exists
if ($token) {
$payload = $this->_authService->validateToken($token);
if ($payload) {
$this->_username = $payload['username'];
$this->_userId = $payload['uid'];
$this->_accessLevel = $payload['accessLevel'];
$this->_loggedIn = true;
}
}
// Start session only if needed for CAPTCHA
if (session_status() === PHP_SESSION_NONE && isset($_GET['page']) && $_GET['page'] === 'register') {
session_start();
}
}
/**
* Sets the user's access level
*
* @param int $level The access level to set (admin = 1, regular user = 2)
* @return void
*/
private function setAccessLevel($level) {
$this->_accessLevel = $level;
$_SESSION['accessLevel'] = $level;
}
/**
* Gets the user's access level
*
* @return int|null The user's access level (admin = 1, regular user = 2) or null if not set
*/
public function getAccessLevel() {
return $this->_accessLevel;
}
/**
* @param $username
* @param $password
* @return bool
* Using a username and password, authenticate a user and assign variables from query
* Authenticates a user using username and password
*
* Checks credentials against the database and generates a JWT token if valid.
* Sets user properties if authentication is successful.
*
* @param string $username The username to authenticate
* @param string $password The password to verify
* @return string|bool JWT token if authentication was successful, false otherwise
*/
public function Authenticate($username, $password): bool
public function Authenticate($username, $password)
{
$users = new UserDataSet();
$userDataSet = $users->checkUserCredentials($username, $password);
$accessLevel = $users->checkAccessLevel($username);
if(count($userDataSet) > 0) {
$_SESSION['login'] = $username;
$_SESSION['uid'] = $userDataSet[0]->getId();
$this->setAccessLevel($accessLevel);
$userData = $userDataSet[0];
$accessLevel = $users->checkAccessLevel($username);
// Generate JWT token
$token = $this->_authService->generateToken([
'id' => $userData->getId(),
'username' => $userData->getUsername(),
'userType' => $accessLevel
]);
// Set user properties
$this->_loggedIn = true;
$this->_username = $username;
$this->_userId = $userDataSet[0]->getId();
return true;
$this->_userId = $userData->getId();
$this->_accessLevel = $accessLevel;
return $token;
}
else {
$this->_loggedIn = false;
@@ -65,26 +136,84 @@ class User {
}
/**
* Logs the user out
*
* Resets all user properties to their default values.
* Note: This doesn't invalidate the JWT token - handled client-side
* by removing the token from storage.
*
* @return void
* Unset user variables from session, and set variables to default values - destroying session.
*/
public function logout() {
unset($_SESSION['login']);
unset($_SESSION['uid']);
// Reset user properties
$this->_loggedIn = false;
$this->_username = "None";
$this->_userId = "0";
session_destroy();
$this->_accessLevel = null;
}
/**
* Checks if the user is currently logged in
*
* @return bool True if the user is logged in, false otherwise
*/
public function isLoggedIn(): bool
{
return $this->_loggedIn;
}
public function __destruct()
/**
* Static method to check if a request is authenticated
*
* This method can be called from any controller to check if the request
* has a valid JWT token. It returns the payload if authenticated or
* sends an error response and returns false if not.
*
* @param bool $required Whether authentication is required (defaults to true)
* @return array|false The payload if authenticated, false otherwise
*/
public static function checkAuth(bool $required = true)
{
$authService = new AuthService();
// Get the token from the Authorization header
$headers = getallheaders();
$token = isset($headers['Authorization']) ? str_replace('Bearer ', '', $headers['Authorization']) : null;
// Validate the token
$payload = $token ? $authService->validateToken($token) : null;
// If authentication is required and no valid token, return error
if ($required && !$payload) {
header('Content-Type: application/json');
http_response_code(401);
echo json_encode(['error' => 'Authentication required']);
return false;
}
return $payload;
}
/**
* Static method to check if a request is from an admin
*
* This method can be called from any controller to check if the request
* has a valid JWT token with admin access level. It returns the payload
* if authenticated as admin or sends an error response and returns false if not.
*
* @return array|false The payload if authenticated as admin, false otherwise
*/
public static function checkAdmin()
{
$payload = self::checkAuth(true);
if ($payload && isset($payload['accessLevel']) && $payload['accessLevel'] == 1) {
return $payload;
}
header('Content-Type: application/json');
http_response_code(403);
echo json_encode(['error' => 'Admin access required']);
return false;
}
}

0
Models/UserData.php Normal file → Executable file
View File

0
Models/UserDataSet.php Normal file → Executable file
View File

152
Views/index.phtml Normal file → Executable file
View File

@@ -1,60 +1,104 @@
<?php require('template/header.phtml') ?>
<div class="row">
<div class="col-5 me-auto">
<p><?php echo $view->dbMessage; ?></p>
<?php
/**
* Main index view for the EcoBuddy application
*
* This file serves as the main view for the application, displaying
* a table of facilities with various actions depending on the user's
* access level. It includes modals for creating, updating, deleting,
* and viewing statuses of facilities.
*
* The table is populated dynamically using JavaScript, with the data
* stored in sessionStorage.
*/
require('template/header.phtml')
?>
<div class="row">
<div class="col-12 p-0" id="facilityContent">
<!-- Main content -->
<div class="card shadow-sm border-0 rounded-3">
<!-- Title and add button (admins only) -->
<div class="card-header bg-light py-3">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<h5 class="mb-0 fw-bold text-primary">
<i class="bi bi-geo-alt-fill me-2 text-success"></i>Facilities
</h5>
<!-- Badge showing the number of facilities -->
<span class="badge bg-success rounded-pill ms-2" id="facilityCount"></span>
</div>
<?php if($view->user->getAccessLevel() == 1): ?>
<!-- Add new facility button (admin only) -->
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#createModal">
<i class="bi bi-plus-circle me-1"></i>Add New Facility
</button>
<?php endif; ?>
</div>
</div>
<!-- Pagination controls -->
<div class="card-footer bg-white py-2">
<?php require('template/pagination.phtml');?>
</div>
<!-- Facilities table -->
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="facilityTable">
<thead class="table-light">
<tr>
<?php if($view->user->getAccessLevel() == 1): ?>
<th class="fw-semibold" style="width: 40px;">ID</th>
<?php else: ?>
<th class="d-none">ID</th>
<?php endif; ?>
<th class="fw-semibold" style="width: 15%;">Title</th>
<th class="fw-semibold text-center" style="width: 10%;">Category</th>
<th class="fw-semibold" style="width: 25%;">Description</th>
<th class="fw-semibold" style="width: 20%;">Address</th>
<th class="fw-semibold text-center" style="width: 8%;" hidden>Postcode</th>
<th class="fw-semibold text-center" style="width: 12%;">Coordinates</th>
<th class="fw-semibold text-center" style="width: 8%;">Contributor</th>
<th class="fw-semibold text-center" style="width: 10%;">Actions</th>
</tr>
</thead>
<tbody class="border-top-0">
<!-- Table content will be dynamically populated by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
<form class="col-auto">
<?php require_once('template/createModal.phtml') ?>
</form>
</div>
</div>
<div class="row">
<div class="container-fluid p-3" id="facilityContent">
<table class="table table-bordered">
<thead>
<tr>
<th>Facility ID</th>
<th>Title</th>
<th>Category</th>
<th>Status</th>
<th>Description</th>
<th>Address</th>
<th>Postcode</th>
<th>Lat/Long</th>
<th>Contributor</th>
<?php if($view->user->getAccessLevel() == 1): ?>
<th>Actions</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($view->pageData as $facilityData): ?>
<tr>
<td><?= htmlspecialchars($facilityData->getId() ?? 'N/A') ?></td>
<td><?= htmlspecialchars($facilityData->getTitle() ?? 'N/A') ?></td>
<td><?= htmlspecialchars($facilityData->getCategory() ?? 'N/A') ?></td>
<td><?= htmlspecialchars($facilityData->getStatus() ?? 'N/A') ?></td>
<td><?= htmlspecialchars($facilityData->getDescription() ?? 'N/A') ?></td>
<td><?= htmlspecialchars(trim(($facilityData->getHouseNumber() ?? '') . ' ' .
($facilityData->getStreetName() ?? '') . ' ' .
($facilityData->getCounty() ?? '') . ' ' .
($facilityData->getTown() ?? ''))) ?></td>
<td><?= htmlspecialchars($facilityData->getPostcode() ?? 'N/A') ?></td>
<td><?= htmlspecialchars(($facilityData->getLat() ?? 'N/A') . ', ' .
($facilityData->getLng() ?? 'N/A')) ?></td>
<td><?= htmlspecialchars($facilityData->getContributor() ?? 'N/A') ?></td>
<?php if($view->user->getAccessLevel() == 1): ?>
<td class="btn-group">
<?php require("template/updateModal.phtml") ?>
<?php require("template/deleteModal.phtml") ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Include modal templates -->
<?php require('template/createModal.phtml') ?>
<?php require('template/updateModal.phtml') ?>
<?php require('template/deleteModal.phtml') ?>
<?php require('template/statusModal.phtml') ?>
<!-- Script to update the facility count badge -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Update facility count badge based on data in sessionStorage
const updateFacilityCount = () => {
const facilityData = JSON.parse(sessionStorage.getItem('facilityData') || '[]');
const countBadge = document.getElementById('facilityCount');
if (countBadge) {
countBadge.textContent = `${facilityData.length} facilities`;
}
};
// Initial count update when the page loads
updateFacilityCount();
// Listen for changes in facility data to update the count
window.addEventListener('storage', function(e) {
if (e.key === 'facilityData') {
updateFacilityCount();
}
});
});
</script>
<?php require('template/footer.phtml') ?>
<?php require('template/footer.phtml');?>

163
Views/template/createModal.phtml Normal file → Executable file
View File

@@ -1,32 +1,145 @@
<button type="button" class="col btn bg-primary btn-outline-primary text-light" data-bs-toggle="modal" data-bs-target="#createModal">
<span class="bi bi-pen-fill"></span>
</button>
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="updateModalLabel">Add Facility</h5>
<?php if($view->user->getAccessLevel() == 1): ?>
<!-- Create Facility Modal -->
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="createModalLabel">
<i class="bi bi-plus-circle-fill text-success me-2"></i>Add New Facility
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
<form class="form-inline" method="post" action="<?php echo $_SERVER['PHP_SELF']; ?> ">
<input name="titlCreate" class="form-control rounded mb-2" placeholder="Title">
<input name="cateCreate" class="form-control rounded mb-2" placeholder="Category">
<input name="descCreate" class="form-control rounded mb-2" placeholder="Description">
<input name="hnumCreate" class="form-control rounded mb-2" placeholder="House Number">
<input name="strtCreate" class="form-control rounded mb-2" placeholder="Street Name">
<input name="cntyCreate" class="form-control rounded mb-2" placeholder="County">
<input name="townCreate" class="form-control rounded mb-2" placeholder="Town">
<input name="postCreate" class="form-control rounded mb-2" placeholder="Postcode">
<input name="contCreate" class="form-control rounded mb-2" placeholder="Contributor">
</form>
<button type="submit" class="btn bg-primary btn-outline-primary text-light" name="createButton">Add</button>
<div class="modal-body p-4">
<form id="createForm">
<input type="hidden" name="action" value="create">
<div class="mb-3">
<label for="titlCreate" class="form-label">Title</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-tag text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="titlCreate" name="titlCreate" placeholder="Enter facility title" required>
</div>
</div>
<div class="mb-3">
<label for="cateCreate" class="form-label">Category</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-bookmark text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cateCreate" name="cateCreate" placeholder="Enter facility category" required>
</div>
</div>
<div class="mb-3">
<label for="descCreate" class="form-label">Description</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-card-text text-success"></i>
</span>
<textarea class="form-control border-start-0" id="descCreate" name="descCreate" placeholder="Enter facility description" rows="3" required></textarea>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="hnumCreate" class="form-label">House/Building Number</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-house text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="hnumCreate" name="hnumCreate" placeholder="Enter number" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="strtCreate" class="form-label">Street Name</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-signpost text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="strtCreate" name="strtCreate" placeholder="Enter street name" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="townCreate" class="form-label">Town/City</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-building text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="townCreate" name="townCreate" placeholder="Enter town/city" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="cntyCreate" class="form-label">County</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-map text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cntyCreate" name="cntyCreate" placeholder="Enter county" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="postCreate" class="form-label">Postcode</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-mailbox text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="postCreate" name="postCreate" placeholder="Enter postcode" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="latCreate" class="form-label">Latitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="latCreate" name="latCreate" placeholder="Enter latitude" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="lngCreate" class="form-label">Longitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="lngCreate" name="lngCreate" placeholder="Enter longitude" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="contCreate" class="form-label">Contributor</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0 bg-light" id="contCreate" name="contCreate" placeholder="Auto-filled with your username" disabled required>
</div>
<small class="text-muted">This will be automatically filled with your username</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" data-bs-dismiss="modal">Close</button>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="createForm" class="btn btn-success">
<i class="bi bi-plus-circle me-1"></i>Create Facility
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>

41
Views/template/deleteModal.phtml Normal file → Executable file
View File

@@ -1,20 +1,37 @@
<button type="button" class="col btn bg-danger btn-outline-danger text-light" data-bs-toggle="modal" data-bs-target="#deleteModal">
<span class="bi bi-trash-fill">
</button>
<!-- Delete Facility Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Delete Facility Record</h5>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="deleteModalLabel">
<i class="bi bi-trash text-danger me-2"></i>Delete Facility
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="">
<button type="submit" value="delete" class="btn bg-danger btn-outline-danger text-light" name="deleteButton"">Yes</button>
<input type="hidden" name="id" value="<?= $facilityData->getId()?>">
<button type="button" class="btn btn-outline primary btn-primary" data-bs-dismiss="modal">No</button>
<div class="modal-body p-4">
<form id="deleteForm">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="idDelete" value="">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<span>Are you sure you want to delete this facility record? This action cannot be undone.</span>
</div>
<div class="mt-3">
<p class="mb-1 fw-bold">Facility to be deleted:</p>
<p id="deleteConfirmationText" class="text-danger mb-0"></p>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="deleteForm" class="btn btn-danger">
<i class="bi bi-trash me-1"></i>Delete Permanently
</button>
</div>
</div>
</div>
</div>
</div>

92
Views/template/footer.phtml Normal file → Executable file
View File

@@ -1,8 +1,6 @@
</div>
<div class="site-footer fixed-bottom mt-auto">
<div class="col-auto">
<?php require_once('pagination.phtml'); ?>
</div>
<div class="site-footer mt-auto">
<!-- Footer Content -->
<div class="row">
<div id="footer" class="col-xs-12">
<p class="m-0">George Wilkinson @2024</p>
@@ -10,10 +8,92 @@
</div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<!-- script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script -->
<script src="/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Application JavaScript -->
<!-- Note: simpleAuth.js is already included in the header -->
<!-- Note: facilityData.js is already included in the header -->
<script src="/public/js/comments.js"></script>
<!-- Initialize components -->
<script>
// Only run initialization if not already done
if (!window.initializationComplete) {
document.addEventListener('DOMContentLoaded', function() {
// Initialize auth service
const loginButton = document.querySelector('[data-bs-toggle="modal"]');
const loginModal = document.getElementById('loginModal');
// Initialize all modals
try {
const modalElements = document.querySelectorAll('.modal');
modalElements.forEach(modalElement => {
if (modalElement) {
const modalInstance = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true
});
// Add click handler for modal triggers
const triggers = document.querySelectorAll(`[data-bs-target="#${modalElement.id}"]`);
triggers.forEach(trigger => {
trigger.addEventListener('click', (e) => {
e.preventDefault();
modalInstance.show();
});
});
}
});
} catch (error) {
console.error('Error initializing modals:', error);
}
// Initialize auth form handlers
const loginForm = document.querySelector('#loginModal form');
const loginError = document.querySelector('#loginError');
const captchaContainer = document.querySelector('.captcha-container');
if (loginForm) {
// Show CAPTCHA if needed
if (simpleAuth.needsCaptcha() && captchaContainer) {
captchaContainer.style.display = 'flex';
}
}
// Handle logout button
const logoutButton = document.querySelector('button[name="logoutButton"]');
if (logoutButton) {
logoutButton.addEventListener('click', async (e) => {
e.preventDefault();
await simpleAuth.logout();
});
}
// Validate token if authenticated
if (simpleAuth.isAuthenticated()) {
simpleAuth.validateToken().then(valid => {
if (!valid) {
if (!localStorage.getItem('validationAttempted')) {
localStorage.setItem('validationAttempted', 'true');
window.location.reload();
} else {
localStorage.removeItem('validationAttempted');
}
} else {
localStorage.removeItem('validationAttempted');
}
});
}
// Mark initialization as complete
window.initializationComplete = true;
});
}
</script>
</body>
</html>

365
Views/template/header.phtml Normal file → Executable file
View File

@@ -4,100 +4,317 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="description" content="EcoBuddy - Sustainable facilities management platform">
<meta name="author" content="">
<link rel="icon" type="image/x-icon" href="/images/ecoBuddy_x32.png"
<!-- Bootstrap core CSS -->
<link href="/css/bootstrap.css" rel="stylesheet">
<!-- Bootstrap theme -->
<link href="/css/bootstrap-theme.css" rel="stylesheet">
<link href="/css/my-style.css" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="/images/ecoBuddy_x32.png">
<!-- Bootstrap core CSS from CDN for faster loading -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- CSS theme -->
<link href="/public/css/my-style.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="/css/bootstrap-icons.css" rel="stylesheet">
<link href="/public/css/bootstrap-icons.css" rel="stylesheet">
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Dynamic page title based on the current page -->
<title>Ecobuddy - <?php echo $view->pageTitle; ?></title>
<!-- Load simplified authentication helper -->
<script src="/public/js/simpleAuth.js"></script>
<!-- Load API client -->
<script src="/public/js/apiClient.js"></script>
<!-- Load facility data script -->
<script src="/public/js/facilityData.js"></script>
<!-- Initialise facility data from PHP server-side data -->
<script>
<?php if (isset($view->facilityDataSet) && is_array($view->facilityDataSet)): ?>
try {
// Convert PHP data to JavaScript object with proper encoding
// Using JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE for proper character handling
const initialData = <?php echo json_encode($view->facilityDataSet,
JSON_UNESCAPED_SLASHES |
JSON_UNESCAPED_UNICODE |
JSON_PARTIAL_OUTPUT_ON_ERROR
); ?>;
// Validate and store data in sessionStorage for use across the application
if (Array.isArray(initialData) && initialData.length > 0) {
sessionStorage.setItem('facilityData', JSON.stringify(initialData));
// Initialize based on DOM state to ensure scripts run at the right time
if (document.readyState === 'complete' || document.readyState === 'interactive') {
if (typeof initialiseFacilityData === 'function') {
initialiseFacilityData(initialData);
}
} else {
document.addEventListener('DOMContentLoaded', function() {
if (typeof initialiseFacilityData === 'function') {
initialiseFacilityData(initialData);
}
});
}
}
// Add client-side authentication check to update UI
document.addEventListener('DOMContentLoaded', function() {
// Check if user is authenticated on the client side
if (window.auth && window.auth.isAuthenticated()) {
console.log('User is authenticated on client side');
// Get user data
const user = window.auth.getUser();
if (user) {
console.log('User data:', user);
// Hide login button if it exists
const loginButton = document.getElementById('loginButton');
if (loginButton) {
loginButton.style.display = 'none';
}
// Hide login modal if it exists
const loginModal = document.getElementById('loginModal');
if (loginModal) {
loginModal.style.display = 'none';
}
// Show user menu
const userMenuContainer = document.createElement('div');
userMenuContainer.className = 'user-menu';
userMenuContainer.innerHTML = `
<div class="user-avatar">
<i class="bi bi-person-fill text-success"></i>
</div>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" id="userMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
${user.username}
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userMenuButton">
<li><a class="dropdown-item" href="#"><i class="bi bi-person me-2"></i>Profile</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-gear me-2"></i>Settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item text-danger" id="logoutButton"><i class="bi bi-box-arrow-right me-2"></i>Logout</button></li>
</ul>
</div>
`;
// Replace login button with user menu
if (loginButton) {
loginButton.parentNode.replaceChild(userMenuContainer, loginButton);
}
// Add logout button handler
const logoutButton = document.getElementById('logoutButton');
if (logoutButton) {
logoutButton.addEventListener('click', async function() {
await window.auth.logout();
window.location.reload();
});
}
}
}
});
} catch (error) {
console.error('Error processing facility data:', error);
}
<?php endif; ?>
</script>
</head>
<nav class="navbar navbar-expand-lg p-0 m-2 border rounded-2">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03" aria-expanded="false" aria-label="Toggle navigation">
<body role="document">
<!-- Navigation bar -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm sticky-top">
<div class="container-fluid px-3">
<!-- Brand logo and name -->
<a class="navbar-brand d-flex align-items-center" href="/index.php">
<img src="/images/ecoBuddy_x64.png" alt="EcoBuddy Logo" width="48" height="48" class="me-2">
<span class="fw-bold text-success">EcoBuddy</span>
</a>
<!-- Mobile menu toggle -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent"
aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/index.php"><img id="navIcon" class="img-thumbnail bg-transparent border-3 border-success border-opacity-25 rounded my-1 me-2" height="64px" width="64px" src="/images/ecoBuddy_x64.png" alt=""/><span class="pt-5 mb-auto">Ecobuddy</span></a>
<div class="collapse navbar-collapse" id="navbarTogglerDemo03">
<!-- Navigation content -->
<div class="collapse navbar-collapse" id="navbarContent">
<!-- Main navigation links -->
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/index.php">
<i class="bi bi-house-fill me-1"></i>Home
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/map.php">
<i class="bi bi-map-fill me-1"></i>Map
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.php">
<i class="bi bi-info-circle-fill me-1"></i>About
</a>
</li>
</ul>
<form class="row m-0 me-2 align-content-center align-content-center align-items-center" role="search" action="" method="POST">
<div class="col">
<div class="form-floating input-group">
<select name="sort" class="form-select border-3 border-success-subtle" id="sort">
<option value="1" <?php if(isset($_GET['sort']) && $_GET['sort'] == '1') echo 'selected'; ?>>Title</option>
<option value="2" <?php if(isset($_GET['sort']) && $_GET['sort'] == '2') echo 'selected'; ?>>Category</option>
<option value="0" <?php if(isset($_GET['sort']) && $_GET['sort'] == '0') echo 'selected'; ?>>Status</option>
<option value="3" <?php if(isset($_GET['sort']) && $_GET['sort'] == '3') echo 'selected'; ?>>Description</option>
<option value="4" <?php if(isset($_GET['sort']) && $_GET['sort'] == '4') echo 'selected'; ?>>Street Name</option>
<option value="5" <?php if(isset($_GET['sort']) && $_GET['sort'] == '5') echo 'selected'; ?>>County</option>
<option value="6" <?php if(isset($_GET['sort']) && $_GET['sort'] == '6') echo 'selected'; ?>>Town</option>
<option value="7" <?php if(isset($_GET['sort']) && $_GET['sort'] == '7') echo 'selected'; ?>>Postcode</option>
<option value="8" <?php if(isset($_GET['sort']) && $_GET['sort'] == '8') echo 'selected'; ?>>Contributor</option>
</select>
<span class="form-floating input-group">
<select class="form-select border-3 border-start-0 rounded-end border-success-subtle" name="dir" id="dir">
<option value="asc" <?php if($_GET['dir'] == 'asc') echo 'selected'; ?>>Asc</option>
<option value="desc" <?php if($_GET['dir'] == 'desc') echo 'selected'; ?>>Desc</option>
</select>
<label for="dir">Order</label>
<!-- Search and filter controls -->
<div class="d-flex flex-column flex-lg-row search-controls mx-auto">
<form class="d-flex flex-column flex-lg-row gap-2 w-100" role="search" action="" method="POST">
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-sort-alpha-down text-success"></i>
</span>
<label for="sort">Sort By</label>
</div>
</div>
<div class="col">
<div class="form-floating input-group">
<select name="filterCat" class="form-select border-3 border-success-subtle" id="filterCat">
<option value="1" <?php if(isset($_GET['category']) && $_GET['category'] == '1') echo 'selected'; ?>>Title</option>
<option value="2" <?php if(isset($_GET['category']) && $_GET['category'] == '2') echo 'selected'; ?>>Category</option>
<option value="0" <?php if(isset($_GET['category']) && $_GET['category'] == '0') echo 'selected'; ?>>Status</option>
<option value="3" <?php if(isset($_GET['category']) && $_GET['category'] == '3') echo 'selected'; ?>>Description</option>
<option value="4" <?php if(isset($_GET['category']) && $_GET['category'] == '4') echo 'selected'; ?>>Street Name</option>
<option value="5" <?php if(isset($_GET['category']) && $_GET['category'] == '5') echo 'selected'; ?>>County</option>
<option value="6" <?php if(isset($_GET['category']) && $_GET['category'] == '6') echo 'selected'; ?>>Town</option>
<option value="7" <?php if(isset($_GET['category']) && $_GET['category'] == '7') echo 'selected'; ?>>Postcode</option>
<option value="8" <?php if(isset($_GET['category']) && $_GET['category'] == '8') echo 'selected'; ?>>Contributor</option>
<select name="sort" class="form-select border-start-0 filter-control" id="sort">
<option value="title">Title</option>
<option value="category">Category</option>
<option value="status">Status</option>
<option value="description">Description</option>
<option value="streetName">Street</option>
<option value="county">County</option>
<option value="town">Town</option>
<option value="postcode">Postcode</option>
<option value="contributor">Contributor</option>
</select>
<select class="form-select sort-control" name="dir" id="dir" data-order="asc">
<option value="asc">Asc</option>
<option value="desc">Desc</option>
</select>
<span class="input-group-text bi bi-filter-circle bg-success-subtle border-0 rounded-end" id="filterCat"></span>
<label for="filterCat">Column Filter</label>
</div>
</div>
<div class="col">
<div class="form-floating input-group">
<label for="search"></label>
<input placeholder="<?php if(isset($_GET['filter'])) echo $_GET['filter']; ?>" class="form-control border-3 border-success-subtle" id="search" type="search" name="filter" aria-label="Search">
<span class="input-group-text bg-success-subtle border-0 rounded-end" id="search">
<button class="btn bg-light bg-success-subtle" type="submit"><span class="bi bi-search"></span></button>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-filter-circle-fill text-success"></i>
</span>
<select name="filterCat" class="form-select border-start-0 filter-control" id="filterCat">
<option value="">All</option>
<option value="title">Title</option>
<option value="category">Category</option>
<option value="status">Status</option>
<option value="description">Description</option>
<option value="streetName">Street</option>
<option value="county">County</option>
<option value="town">Town</option>
<option value="postcode">Postcode</option>
<option value="contributor">Contributor</option>
</select>
</div>
</div>
</form>
<div class="me-2 ms-2">
<div class="col-sm" id="loginStatus">
<?php
if(!$view->user->isLoggedIn()) {
require_once('Views/template/loginModal.phtml');
}
if($view->user->isLoggedIn()) {
require_once('Views/template/logoutButton.phtml');
}
?>
</div>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-search text-success"></i>
</span>
<input class="form-control border-start-0" id="searchInput" type="search" name="filter" placeholder="Search..." aria-label="Search">
</div>
</form>
</div>
<!-- User account section -->
<div class="ms-lg-3 mt-3 mt-lg-0">
<?php if(!$view->user->isLoggedIn()): ?>
<button type="button" class="btn btn-success" id="loginButton" data-bs-toggle="modal" data-bs-target="#loginModal">
<i class="bi bi-box-arrow-in-right me-1"></i>Login
</button>
<?php else: ?>
<div class="user-menu">
<div class="user-avatar">
<i class="bi bi-person-fill text-success"></i>
</div>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" id="userMenuButton" data-bs-toggle="dropdown" aria-expanded="false">
<?php echo $view->user->getUsername(); ?>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userMenuButton">
<li><a class="dropdown-item" href="#"><i class="bi bi-person me-2"></i>Profile</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-gear me-2"></i>Settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item text-danger" id="logoutButton"><i class="bi bi-box-arrow-right me-2"></i>Logout</button></li>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</nav>
<body role="document">
<div class="main container-fluid">
<div class="col" id="content">
<!-- Login Modal -->
<?php if(!$view->user->isLoggedIn()): ?>
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="loginModalLabel">
<i class="bi bi-box-arrow-in-right text-success me-2"></i>Login to EcoBuddy
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="username" name="username" placeholder="Enter your username" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-lock text-success"></i>
</span>
<input type="password" class="form-control border-start-0" id="password" name="password" placeholder="Enter your password" required>
</div>
</div>
<div id="loginError" class="alert alert-danger" style="display: none;"></div>
<div class="row captcha-container" style="display: none;">
<!-- CAPTCHA Display -->
<div class="col-md-6 mb-3">
<label for="captchaCode" class="form-label">CAPTCHA Code</label>
<input type="text" class="form-control bg-light" id="captchaCode" name="generatedCaptcha" value="" readonly>
</div>
<!-- CAPTCHA Input -->
<div class="col-md-6 mb-3">
<label for="captchaInput" class="form-label">Enter CAPTCHA</label>
<input type="text" class="form-control" id="captchaInput" name="captchaInput" placeholder="Enter code">
</div>
</div>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-success">
<i class="bi bi-box-arrow-in-right me-2"></i>Login
</button>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between align-items-center">
<small class="text-muted">Don't have an account? <a href="" onclick="alert('Please contact the administrator to create an account.');" class="text-success">Register</a></small>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- Main content container -->
<div class="container-fluid py-4 px-3">
<div class="row" id="content">

0
Views/template/loginError.phtml Normal file → Executable file
View File

74
Views/template/loginModal.phtml Normal file → Executable file
View File

@@ -1,38 +1,62 @@
<button type="button" class="btn bg-primary btn-outline-primary text-light m-auto" data-bs-toggle="modal"
data-bs-target="#loginModal">
Login
</button>
<?= isset($view->loginError) ? '<div class="modal-backdrop fade show"></div>' : '' ?>
<div class="modal fade <?= isset($view->loginError) ? 'show' : '' ?>" id="loginModal" tabindex="-1"
aria-labelledby="loginModalLabel" aria-hidden="<?= isset($view->loginError) ? 'false' : 'true' ?>"
style="<?= isset($view->loginError) ? 'display: block;' : '' ?>">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginModalLabel">Login</h5>
<!-- Login Modal -->
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="loginModalLabel">
<i class="bi bi-box-arrow-in-right text-success me-2"></i>Login to EcoBuddy
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
<div class="modal-body p-4">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Username"
required>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="username" name="username" placeholder="Enter your username" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password"
required>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-lock text-success"></i>
</span>
<input type="password" class="form-control border-start-0" id="password" name="password" placeholder="Enter your password" required>
</div>
</div>
<div id="loginError" class="alert alert-danger" style="display: none;"></div>
<div class="row captcha-container" style="display: none;">
<!-- CAPTCHA Display -->
<div class="col-md-6 mb-3">
<label for="captchaCode" class="form-label">CAPTCHA Code</label>
<input type="text" class="form-control bg-light" id="captchaCode" name="generatedCaptcha" value="" readonly>
</div>
<!-- CAPTCHA Input -->
<div class="col-md-6 mb-3">
<label for="captchaInput" class="form-label">Enter CAPTCHA</label>
<input type="text" class="form-control" id="captchaInput" name="captchaInput" placeholder="Enter code">
</div>
</div>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-success">
<i class="bi bi-box-arrow-in-right me-2"></i>Login
</button>
</div>
<?php if (isset($view->loginError)) { include('Views/template/loginError.phtml');} ?>
<button type="submit" class="btn bg-primary btn-outline-primary text-light" name="loginButton">Login
</button>
</form>
</div>
<div class="modal-footer">
<a href="/index.php <?php unset($_GET['modal'])?>" type="button" class="btn btn-warning btn-outline-warning text-light" data-bs-dismiss="modal">
Close
</a>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between align-items-center">
<small class="text-muted">Don't have an account? <a href="" onclick="alert('Please contact the administrator to create an account.');" class="text-success">Register</a></small>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

6
Views/template/logoutButton.phtml Normal file → Executable file
View File

@@ -1,4 +1,2 @@
<form class="form-floating my-auto" method="post" action="<?php echo $_SERVER['PHP_SELF']; ?> ">
<?php echo "<p class='text-center bg-light border-0 rounded mb-1' style='color: black;'>" . $user->getUsername() . "<span class='bi bi-person-fill'></span></p>"?>
<button class="btn bg-danger btn-outline-danger text-light" type="submit" name="logoutButton">Logout</button>
</form>
<?php echo "<p class='text-center bg-light border-0 rounded mb-1' style='color: black;'>" . $user->getUsername() . "<span class='bi bi-person-fill'></span></p>"?>
<button class="btn bg-danger btn-outline-danger text-light" type="button" id="logoutButton">Logout</button>

111
Views/template/pagination.phtml Normal file → Executable file
View File

@@ -1,37 +1,78 @@
<div>
<div class="row mb-2">
<!-- Form for Pagination -->
<div id="paginationButtons" class="col-auto m-auto btn-group">
<?php
$param = $_GET;
unset($param['page']); // Remove the page parameter to avoid duping
function buildUrl($page, $param): string
{
$param['page'] = $page;
return '?' . http_build_query($param);
}
?>
<!-- Start Button -->
<a class="btn btn-outline-primary" href="<?= buildUrl(0, $param) ?>0" <?= $view->pageNumber <= 0 ? 'disabled' : '' ?>><i class="bi bi-chevron-double-left"></i> Start</a>
<!-- Back Button -->
<a class="btn btn-outline-primary" href="<?= buildUrl(max($view->pageNumber - 1, 0), $param)?> " <?= $view->pageNumber <= 0 ? 'disabled' : '' ?>><i class="bi bi-chevron-left"></i> Back</a>
<!-- Dynamic Page Buttons -->
<?php
$totalPages = $view->paginator->getTotalPages();
for ($i = $view->pageNumber - 2; $i <= $view->pageNumber + 2; $i++) {
if ($i >= 0 && $i < $totalPages): ?>
<a href="<?= buildUrl($i, $param) ?>"
class="btn <?= $i === $view->pageNumber ? 'btn-dark' : 'btn-outline-primary' ?>"
<?= $i === $view->pageNumber ? 'disabled' : '' ?>>
<?= $i + 1 ?>
</a>
<?php endif;
} ?>
<!-- Forward Button -->
<a class="btn btn-outline-primary" href="<?=buildUrl(min($view->pageNumber + 1, $totalPages), $param)?>" <?= $view->pageNumber >= $totalPages - 1 ? 'disabled' : '' ?>>Forward <i class="bi bi-chevron-right"></i></a>
<!-- End Button -->
<a class="btn btn-outline-primary" href="<?= buildUrl($totalPages - 1, $param) ?>"<?= $view->pageNumber >= $totalPages - 1 ? 'disabled' : '' ?>>End <i class="bi bi-chevron-double-right"></i></a>
</div>
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center gap-2">
<div class="text-muted small">
<span id="paginationInfo" class="d-flex align-items-center">
<i class="bi bi-info-circle me-2 text-success"></i>
<span>Showing facilities</span>
</span>
</div>
<!-- Pagination controls -->
<nav aria-label="Facility table pagination">
<ul class="pagination pagination-sm mb-0" id="paginationControls">
<!-- First page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="First" id="firstPage">
<i class="bi bi-chevron-double-left"></i>
</a>
</li>
<!-- Previous page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="Previous" id="prevPage">
<i class="bi bi-chevron-left"></i>
</a>
</li>
<!-- Dynamic page numbers will be inserted here as list items -->
<!-- Next page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="Next" id="nextPage">
<i class="bi bi-chevron-right"></i>
</a>
</li>
<!-- Last page button -->
<li class="page-item">
<a class="page-link border-0 text-success" href="#" aria-label="Last" id="lastPage">
<i class="bi bi-chevron-double-right"></i>
</a>
</li>
</ul>
</nav>
<!-- Items per page selector -->
<div class="d-flex align-items-center">
<label for="itemsPerPage" class="form-label text-muted small mb-0 me-2">Items per page:</label>
<select class="form-select form-select-sm" id="itemsPerPage" style="width: 70px;">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Set up items per page selector
const itemsPerPageSelect = document.getElementById('itemsPerPage');
if (itemsPerPageSelect) {
itemsPerPageSelect.addEventListener('change', function() {
// Update items per page in the pagination system
if (typeof itemsPerPage !== 'undefined') {
itemsPerPage = parseInt(this.value);
currentPage = 1; // Reset to first page
// Recalculate total pages
if (typeof filteredData !== 'undefined' && typeof totalPages !== 'undefined') {
totalPages = Math.ceil(filteredData.length / itemsPerPage);
// Update table with new pagination
if (typeof updateTableWithPagination === 'function') {
updateTableWithPagination();
}
}
}
});
}
});
</script>

107
Views/template/statusModal.phtml Executable file
View File

@@ -0,0 +1,107 @@
<!-- Facility Comments Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="statusModalLabel">
<i class="bi bi-chat-square-text text-primary me-2"></i>Facility Comments
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<!-- Add Comment Form (Only shown to logged in users) -->
<?php if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true && $_SESSION['access'] >= 1): ?>
<div class="mb-4 border-bottom pb-4">
<h6 class="fw-bold mb-3">
<i class="bi bi-plus-circle text-success me-2"></i>Add New Comment
</h6>
<form id="commentForm">
<input type="hidden" name="action" value="status">
<input type="hidden" name="facilityId" id="commentFacilityId" value="">
<div class="mb-3">
<label for="commentText" class="form-label">Your Comment</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-pencil text-primary"></i>
</span>
<textarea class="form-control border-start-0" id="commentText" name="commentText" rows="3" placeholder="Share your thoughts about this facility..." required></textarea>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="bi bi-send me-1"></i>Post Comment
</button>
</div>
</form>
</div>
<?php endif; ?>
<!-- Existing Comments Section -->
<div>
<h6 class="fw-bold mb-3">
<i class="bi bi-chat-square-dots text-primary me-2"></i>Comments
</h6>
<div id="commentsContainer" class="comments-container">
<!-- Comments will be loaded here dynamically -->
<div class="text-center py-4 text-muted" id="noCommentsMessage">
<i class="bi bi-chat-square-text fs-4 d-block mb-2"></i>
<p>No comments yet. Be the first to share your thoughts!</p>
</div>
</div>
<!-- Login Prompt for Non-Authenticated Users -->
<?php if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true): ?>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle me-2"></i>
<span>Please <a href="#" data-bs-toggle="modal" data-bs-target="#loginModal" data-bs-dismiss="modal">log in</a> to add your comments.</span>
</div>
<?php endif; ?>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Edit Comment Modal -->
<div class="modal fade" id="editCommentModal" tabindex="-1" aria-labelledby="editCommentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="editCommentModalLabel">
<i class="bi bi-pencil-square text-primary me-2"></i>Edit Comment
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<form id="editCommentForm">
<input type="hidden" name="action" value="editComment">
<input type="hidden" name="commentId" id="editCommentId" value="">
<div class="mb-3">
<label for="editCommentText" class="form-label">Edit Your Comment</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-pencil text-primary"></i>
</span>
<textarea class="form-control border-start-0" id="editCommentText" name="editCommentText" rows="4" placeholder="Update your comment..." required></textarea>
</div>
</div>
</form>
</div>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="editCommentForm" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Save Changes
</button>
</div>
</div>
</div>
</div>
</div>

158
Views/template/updateModal.phtml Normal file → Executable file
View File

@@ -1,31 +1,143 @@
<button type="button" class="col btn bg-primary btn-outline-primary text-light" data-bs-toggle="modal" data-bs-target="#updateModal">
<span class="bi bi-pen-fill"></span>
</button>
<!-- Update Facility Modal -->
<div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="updateModalLabel">Update Facility</h5>
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-light">
<h5 class="modal-title" id="updateModalLabel">
<i class="bi bi-pencil-square text-success me-2"></i>Update Facility
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
<input name="titlUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getTitle() ?? '' ?>" placeholder="Title">
<input name="cateUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getCategory() ?? '' ?>" placeholder="Category">
<input name="descUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getDescription() ?? '' ?>" placeholder="Description">
<input name="hnumUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getHouseNumber() ?? '' ?>" placeholder="House Number">
<input name="strtUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getStreetName() ?? '' ?>" placeholder="Street Name">
<input name="cntyUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getCounty() ?? '' ?>" placeholder="County">
<input name="townUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getTown() ?? '' ?>" placeholder="Town">
<input name="postUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getPostcode() ?? '' ?>" placeholder="Postcode">
<input name="contUpdate" class="form-control rounded mb-2" value="<?= $facilityData->getContributor() ?? '' ?>" placeholder="Contributor">
<button type="submit" class="btn bg-primary btn-outline-primary text-light" name="updateButton">Update</button>
<input type="hidden" name="idUpdate" value="<?= $facilityData->getId()?>">
<div class="modal-body p-4">
<form id="updateForm">
<input type="hidden" name="action" value="update">
<input type="hidden" name="idUpdate" value="">
<div class="mb-3">
<label for="titlUpdate" class="form-label">Title</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-tag text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="titlUpdate" name="titlUpdate" placeholder="Enter facility title" required>
</div>
</div>
<div class="mb-3">
<label for="cateUpdate" class="form-label">Category</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-bookmark text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cateUpdate" name="cateUpdate" placeholder="Enter facility category" required>
</div>
</div>
<div class="mb-3">
<label for="descUpdate" class="form-label">Description</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-card-text text-success"></i>
</span>
<textarea class="form-control border-start-0" id="descUpdate" name="descUpdate" placeholder="Enter facility description" rows="3" required></textarea>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="hnumUpdate" class="form-label">House/Building Number</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-house text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="hnumUpdate" name="hnumUpdate" placeholder="Enter number" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="strtUpdate" class="form-label">Street Name</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-signpost text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="strtUpdate" name="strtUpdate" placeholder="Enter street name" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="townUpdate" class="form-label">Town/City</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-building text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="townUpdate" name="townUpdate" placeholder="Enter town/city" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="cntyUpdate" class="form-label">County</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-map text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="cntyUpdate" name="cntyUpdate" placeholder="Enter county" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="postUpdate" class="form-label">Postcode</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-mailbox text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="postUpdate" name="postUpdate" placeholder="Enter postcode" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="latUpdate" class="form-label">Latitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="latUpdate" name="latUpdate" placeholder="Enter latitude" required>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="lngUpdate" class="form-label">Longitude</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-geo-alt text-success"></i>
</span>
<input type="text" class="form-control border-start-0" id="lngUpdate" name="lngUpdate" placeholder="Enter longitude" required>
</div>
</div>
</div>
<div class="mb-3">
<label for="contUpdate" class="form-label">Contributor</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<i class="bi bi-person text-success"></i>
</span>
<input type="text" class="form-control border-start-0 bg-light" id="contUpdate" name="contUpdate" placeholder="Original contributor" readonly required>
</div>
<small class="text-muted">Original contributor of this facility</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" data-bs-dismiss="modal">Close</button>
<div class="modal-footer bg-light">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="updateForm" class="btn btn-success">
<i class="bi bi-check-circle me-1"></i>Update Facility
</button>
</div>
</div>
</div>
</div>

225
add_facilities.py Normal file
View File

@@ -0,0 +1,225 @@
import sqlite3
import random
# Connect to the SQLite database
conn = sqlite3.connect('Databases/ecobuddy.sqlite')
cursor = conn.cursor()
# Check if we need to add any new categories
cursor.execute("SELECT id FROM ecoCategories")
existing_categories = [row[0] for row in cursor.fetchall()]
# Add two new categories
new_categories = [
(16, "Urban Farms"),
(17, "Rainwater Harvesting Systems")
]
for category in new_categories:
if category[0] not in existing_categories:
cursor.execute("INSERT INTO ecoCategories (id, name) VALUES (?, ?)", category)
print(f"Added new category: {category[1]}")
# Get list of user IDs for contributors
cursor.execute("SELECT id FROM ecoUser")
user_ids = [row[0] for row in cursor.fetchall()]
# Define 10 new ecological facilities in the UK with accurate location data
new_facilities = [
{
"title": "Community Garden Hackney",
"category": 12, # Pollinator Gardens
"description": "Urban garden with native plants to support local pollinators",
"houseNumber": "45",
"streetName": "Dalston Lane",
"county": "Greater London",
"town": "London",
"postcode": "E8 3AH",
"lng": -0.0612,
"lat": 51.5476,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded with new wildflower section",
"Volunteer days every Saturday"
]
},
{
"title": "Rooftop Solar Farm",
"category": 8, # Green Roofs
"description": "Combined green roof and solar panel installation on commercial building",
"houseNumber": "120",
"streetName": "Deansgate",
"county": "Greater Manchester",
"town": "Manchester",
"postcode": "M3 2QJ",
"lng": -2.2484,
"lat": 53.4808,
"contributor": random.choice(user_ids),
"status_comments": [
"Generates power for the entire building"
]
},
{
"title": "Edinburgh Tool Library",
"category": 15, # Community Tool Libraries
"description": "Borrow tools instead of buying them - reducing waste and consumption",
"houseNumber": "25",
"streetName": "Leith Walk",
"county": "Edinburgh",
"town": "Edinburgh",
"postcode": "EH6 8LN",
"lng": -3.1752,
"lat": 55.9677,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Cardiff Bay Water Refill Station",
"category": 9, # Public Water Refill Stations
"description": "Free water refill station to reduce plastic bottle usage",
"houseNumber": "3",
"streetName": "Mermaid Quay",
"county": "Cardiff",
"town": "Cardiff",
"postcode": "CF10 5BZ",
"lng": -3.1644,
"lat": 51.4644,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently cleaned and maintained",
"High usage during summer months"
]
},
{
"title": "Bristol Urban Farm",
"category": 16, # Urban Farms (new category)
"description": "Community-run urban farm providing local produce and education",
"houseNumber": "18",
"streetName": "Stapleton Road",
"county": "Bristol",
"town": "Bristol",
"postcode": "BS5 0RA",
"lng": -2.5677,
"lat": 51.4635,
"contributor": random.choice(user_ids),
"status_comments": [
"Open for volunteers Tuesday-Sunday"
]
},
{
"title": "Newcastle Rainwater Collection System",
"category": 17, # Rainwater Harvesting Systems (new category)
"description": "Public demonstration of rainwater harvesting for garden irrigation",
"houseNumber": "55",
"streetName": "Northumberland Street",
"county": "Tyne and Wear",
"town": "Newcastle upon Tyne",
"postcode": "NE1 7DH",
"lng": -1.6178,
"lat": 54.9783,
"contributor": random.choice(user_ids),
"status_comments": []
},
{
"title": "Brighton Beach Solar Bench",
"category": 7, # Solar-Powered Benches
"description": "Solar-powered bench with USB charging ports and WiFi",
"houseNumber": "",
"streetName": "Kings Road",
"county": "East Sussex",
"town": "Brighton",
"postcode": "BN1 2FN",
"lng": -0.1426,
"lat": 50.8214,
"contributor": random.choice(user_ids),
"status_comments": [
"Popular spot for tourists",
"One USB port currently not working"
]
},
{
"title": "Leeds Community Compost Hub",
"category": 6, # Community Compost Bins
"description": "Large-scale community composting facility for local residents",
"houseNumber": "78",
"streetName": "Woodhouse Lane",
"county": "West Yorkshire",
"town": "Leeds",
"postcode": "LS2 9JT",
"lng": -1.5491,
"lat": 53.8067,
"contributor": random.choice(user_ids),
"status_comments": [
"Recently expanded capacity"
]
},
{
"title": "Glasgow EV Charging Hub",
"category": 4, # Public EV Charging Stations
"description": "Multi-vehicle EV charging station with fast chargers",
"houseNumber": "42",
"streetName": "Buchanan Street",
"county": "Glasgow",
"town": "Glasgow",
"postcode": "G1 3JX",
"lng": -4.2526,
"lat": 55.8621,
"contributor": random.choice(user_ids),
"status_comments": [
"6 charging points available",
"24/7 access"
]
},
{
"title": "Oxford E-Waste Collection Center",
"category": 13, # E-Waste Collection Bins
"description": "Dedicated facility for proper disposal and recycling of electronic waste",
"houseNumber": "15",
"streetName": "St Aldate's",
"county": "Oxfordshire",
"town": "Oxford",
"postcode": "OX1 1BX",
"lng": -1.2577,
"lat": 51.7520,
"contributor": random.choice(user_ids),
"status_comments": []
}
]
# Insert facilities into the database
for facility in new_facilities:
cursor.execute("""
INSERT INTO ecoFacilities
(title, category, description, houseNumber, streetName, county, town, postcode, lng, lat, contributor)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
facility["title"],
facility["category"],
facility["description"],
facility["houseNumber"],
facility["streetName"],
facility["county"],
facility["town"],
facility["postcode"],
facility["lng"],
facility["lat"],
facility["contributor"]
))
# Get the ID of the inserted facility
facility_id = cursor.lastrowid
# Add status comments if any
for comment in facility["status_comments"]:
cursor.execute("""
INSERT INTO ecoFacilityStatus (facilityId, statusComment)
VALUES (?, ?)
""", (facility_id, comment))
print(f"Added facility: {facility['title']} in {facility['town']}")
# Commit the changes and close the connection
conn.commit()
conn.close()
print("\nSuccessfully added 10 new ecological facilities to the database.")

33
api/admin.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* Admin-only API endpoint example
*
* This endpoint demonstrates how to protect an API route for admin users only
* using our simplified authentication approach.
*/
require_once('../Models/User.php');
// Set content type to JSON
header('Content-Type: application/json');
// Check if user is an admin
$auth = User::checkAdmin();
if (!$auth) {
// The checkAdmin method already sent the error response
exit;
}
// User is an admin, proceed with the admin-only logic
$response = [
'status' => 'success',
'message' => 'You have access to this admin-only endpoint',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username'],
'accessLevel' => $auth['accessLevel']
]
];
// Send response
echo json_encode($response);

51
api/login.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
/**
* Login API endpoint
*
* This endpoint handles user authentication and returns a JWT token
* if the credentials are valid.
*/
require_once('../Models/User.php');
// Set content type to JSON
header('Content-Type: application/json');
// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
// Get JSON request body
$json = file_get_contents('php://input');
$data = json_decode($json, true);
// Validate request data
if (!$data || !isset($data['username']) || !isset($data['password'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid request data']);
exit;
}
// Authenticate user
$user = new User();
$token = $user->Authenticate($data['username'], $data['password']);
if ($token) {
// Authentication successful
echo json_encode([
'success' => true,
'token' => $token,
'user' => [
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]
]);
} else {
// Authentication failed
http_response_code(401);
echo json_encode(['error' => 'Invalid credentials']);
}

33
api/protected.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
/**
* Protected API endpoint example
*
* This endpoint demonstrates how to protect an API route using
* our simplified authentication approach.
*/
require_once('../Models/User.php');
// Set content type to JSON
header('Content-Type: application/json');
// Check if user is authenticated
$auth = User::checkAuth();
if (!$auth) {
// The checkAuth method already sent the error response
exit;
}
// User is authenticated, proceed with the endpoint logic
$response = [
'status' => 'success',
'message' => 'You have access to this protected endpoint',
'user' => [
'id' => $auth['uid'],
'username' => $auth['username'],
'accessLevel' => $auth['accessLevel']
]
];
// Send response
echo json_encode($response);

113
auth.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
require_once('Models/AuthService.php');
require_once('Models/UserDataSet.php');
require_once('Models/User.php');
// Enable CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Content-Type: application/json');
// Handle OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
try {
$auth = new AuthService();
$userDataSet = new UserDataSet();
// Handle POST request for login
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
// Handle token refresh
if (isset($data['action']) && $data['action'] === 'refresh') {
if (!isset($data['refreshToken'])) {
http_response_code(400);
echo json_encode(['error' => 'Refresh token is required']);
exit;
}
$refreshToken = $data['refreshToken'];
$newToken = $auth->refreshToken($refreshToken);
if (!$newToken) {
http_response_code(401);
echo json_encode(['error' => 'Invalid or expired refresh token']);
exit;
}
echo json_encode([
'success' => true,
'token' => $newToken
]);
exit;
}
// Handle login
if (!isset($data['username']) || !isset($data['password'])) {
http_response_code(400);
echo json_encode(['error' => 'Username and password are required']);
exit;
}
// Authenticate user
$user = new User();
$token = $user->Authenticate($data['username'], $data['password']);
if ($token) {
// Generate refresh token
$refreshToken = $auth->generateRefreshToken([
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]);
echo json_encode([
'success' => true,
'token' => $token,
'refreshToken' => $refreshToken,
'user' => [
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]
]);
} else {
http_response_code(401);
echo json_encode(['error' => 'Invalid credentials']);
}
exit;
}
// Handle GET request for token validation
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$auth = User::checkAuth(false);
if ($auth) {
echo json_encode([
'valid' => true,
'user' => [
'id' => $auth['uid'],
'username' => $auth['username'],
'accessLevel' => $auth['accessLevel']
]
]);
} else {
http_response_code(401);
echo json_encode(['valid' => false, 'error' => 'Invalid or expired token']);
}
exit;
}
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
} catch (Exception $e) {
error_log('Auth error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error', 'message' => $e->getMessage()]);
}

4085
css/bootstrap-grid.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,597 +0,0 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,594 +0,0 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,347 +0,0 @@
/*!
* Bootstrap v3.1.1 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
text-shadow: 0 1px 0 #fff;
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #2b669a;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #2d6ca2;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #2d6ca2;
border-color: #2b669a;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #3e8f3e;
}
.btn-success:hover,
.btn-success:focus {
background-color: #419641;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #419641;
border-color: #3e8f3e;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #28a4c9;
}
.btn-info:hover,
.btn-info:focus {
background-color: #2aabd2;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #2aabd2;
border-color: #28a4c9;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #e38d13;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #eb9316;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #eb9316;
border-color: #e38d13;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #b92c28;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #c12e2a;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #c12e2a;
border-color: #b92c28;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e8e8e8;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-color: #357ebd;
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
background-repeat: repeat-x;
}
.navbar-default {
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
}
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}
.navbar-inverse {
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
}
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
background-image: linear-gradient(to bottom, #222 0%, #282828 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
background-repeat: repeat-x;
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
}
.alert-success {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
background-repeat: repeat-x;
border-color: #b2dba1;
}
.alert-info {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
background-repeat: repeat-x;
border-color: #9acfea;
}
.alert-warning {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
background-repeat: repeat-x;
border-color: #f5e79e;
}
.alert-danger {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
background-repeat: repeat-x;
border-color: #dca7a7;
}
.progress {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar {
background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-success {
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-info {
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-warning {
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
background-repeat: repeat-x;
}
.progress-bar-danger {
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
background-repeat: repeat-x;
}
.list-group {
border-radius: 4px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #3071a9;
background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
background-repeat: repeat-x;
border-color: #3278b3;
}
.panel {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}
.panel-default > .panel-heading {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-repeat: repeat-x;
}
.panel-primary > .panel-heading {
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
background-repeat: repeat-x;
}
.panel-success > .panel-heading {
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
background-repeat: repeat-x;
}
.panel-info > .panel-heading {
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
background-repeat: repeat-x;
}
.panel-warning > .panel-heading {
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
background-repeat: repeat-x;
}
.panel-danger > .panel-heading {
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
background-repeat: repeat-x;
}
.well {
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
background-repeat: repeat-x;
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
}
/*# sourceMappingURL=bootstrap-theme.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12057
css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12030
css/bootstrap.rtl.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,68 +0,0 @@
nav, #loginStatus, #filters {
background-color: #3cc471;
color: #111
}
#content.full-height {
/*height: calc(100vh - 413px);*/
flex: 1 0 auto;
}
.main {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.facilityContent {
overflow-y: auto;
}
#title {
margin-top: 12px;
background-color: #fff;
color: #000;
}
#menu {
border-top: solid 6px #000;
background-color: #fff;
color: #fff;
height: 400px;
}
#menu a {
/*background-color: #f00;*/
color: #fff;
text-decoration: none;
display: block;
}
#menu a:hover {
/*background-color: #f00;*/
color: #ddd;
text-decoration:underline;
display: block;
}
#content {
background-color: #fff;
/*border-top: solid 6px #f00;*/
}
#footer {
margin-top: 20px;
text-align: center;
background-color: #bbb;
color: #111;
}
.modal {
z-index: 1055
}
.modal-backdrop {
z-index: 1040;
}
.site-footer {
flex: 0 0 auto;
}

16988
debug.log Normal file

File diff suppressed because it is too large Load Diff

192
facilitycontroller.php Normal file
View File

@@ -0,0 +1,192 @@
<?php
require_once('Models/AuthService.php');
require_once('Models/FacilityDataSet.php');
require_once('Models/User.php');
// Enable CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Content-Type: application/json');
// Handle OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
try {
$facilityDataSet = new FacilityDataSet();
// Handle POST requests for CRUD operations
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
// Set up request data
$request = [
'action' => $action,
'data' => $_POST
];
// Apply different authentication checks based on action
if ($action === 'read' || $action === 'getStatuses') {
// These actions don't require authentication
// No authentication check needed
} else if (in_array($action, ['create', 'update', 'delete', 'editStatus', 'deleteStatus'])) {
// These actions require admin privileges
$auth = User::checkAdmin();
if (!$auth) {
// The checkAdmin method already sent the error response
exit;
}
} else if ($action === 'status') {
// This action requires authentication but not admin privileges
$auth = User::checkAuth();
if (!$auth) {
// The checkAuth method already sent the error response
exit;
}
} else {
// Unknown action
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
exit;
}
// Process the action
switch ($action) {
case 'read':
$facilities = $facilityDataSet->fetchAll();
if ($facilities) {
echo json_encode(['success' => true, 'facilities' => $facilities]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to fetch facilities']);
}
break;
case 'create':
try {
$data = [
'title' => $_POST['title'],
'category' => $_POST['category'],
'description' => $_POST['description'],
'houseNumber' => $_POST['houseNumber'],
'streetName' => $_POST['streetName'],
'county' => $_POST['county'],
'town' => $_POST['town'],
'postcode' => $_POST['postcode'],
'lng' => $_POST['lng'],
'lat' => $_POST['lat'],
'contributor' => $auth['username']
];
$facility = $facilityDataSet->createFacility($data);
if ($facility) {
echo json_encode(['success' => true, 'facility' => $facility]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to create facility']);
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'update':
try {
$id = $_POST['id'];
$data = [
'title' => $_POST['title'],
'category' => $_POST['category'],
'description' => $_POST['description'],
'houseNumber' => $_POST['houseNumber'],
'streetName' => $_POST['streetName'],
'county' => $_POST['county'],
'town' => $_POST['town'],
'postcode' => $_POST['postcode'],
'lng' => $_POST['lng'],
'lat' => $_POST['lat'],
'contributor' => $auth['username']
];
$facility = $facilityDataSet->updateFacility($id, $data);
if ($facility) {
echo json_encode(['success' => true, 'facility' => $facility]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to update facility']);
}
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'delete':
$id = $_POST['id'];
if ($facilityDataSet->deleteFacility($id)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to delete facility']);
}
break;
case 'status':
$facilityId = $_POST['facilityId'];
$statusComment = $_POST['statusComment'];
if ($facilityDataSet->addFacilityStatus($facilityId, $statusComment)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to add status']);
}
break;
case 'getStatuses':
$facilityId = $_POST['facilityId'];
$statuses = $facilityDataSet->getFacilityStatuses($facilityId);
echo json_encode(['success' => true, 'statuses' => $statuses]);
break;
case 'editStatus':
$statusId = $_POST['statusId'];
$statusComment = $_POST['statusComment'];
if ($facilityDataSet->updateFacilityStatus($statusId, $statusComment)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to update status']);
}
break;
case 'deleteStatus':
$statusId = $_POST['statusId'];
if ($facilityDataSet->deleteFacilityStatus($statusId)) {
echo json_encode(['success' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to delete status']);
}
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Invalid action']);
break;
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
} catch (Exception $e) {
error_log('Facility controller error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Server error', 'message' => $e->getMessage()]);
}

0
images/ecoBuddy_x128.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

0
images/ecoBuddy_x128.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

0
images/ecoBuddy_x32.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

0
images/ecoBuddy_x32.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
images/ecoBuddy_x64.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

0
images/ecoBuddy_x64.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

14
index.php Normal file → Executable file
View File

@@ -1,13 +1,23 @@
<?php
// load dataset
require_once('Models/UserDataSet.php');
require_once('Models/FacilityDataSet.php');
// make a view class
$view = new stdClass();
$view->pageTitle = 'Home';
// load login controller and pagination controller
// initialise facility data
$facilityDataSet = new FacilityDataSet();
$view->facilityDataSet = $facilityDataSet->fetchAll();
// Log any critical errors
if ($view->facilityDataSet === false) {
error_log('Error fetching facility data');
}
// load login controller
require_once("logincontroller.php");
require_once('paginationcontroller.php');
$view->user = new User();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4447
js/bootstrap.esm.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4494
js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
js/bootstrap.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

194
logincontroller.php Normal file → Executable file
View File

@@ -1,52 +1,158 @@
<?php
require_once("Models/User.php");
require_once("Models/AuthService.php");
// create user and dataset object
$user = new User();
$userDataSet = new UserDataSet();
// Enable CORS
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if (isset($_POST["loginButton"])) {
$username = $_POST["username"];
// hash password
$password = (hash("sha256", $_POST["password"]));
// if login error, show captcha
if (isset($view->loginError)) {
$generatedCaptcha = $_POST["generatedCaptcha"];
$userCaptcha = $_POST["captcha"];
// if captcha wrong, say so
if ($generatedCaptcha !== $userCaptcha) {
$view->loginError = "Incorrect CAPTCHA.";
return;
}
}
// create a new student dataset object that we can generate data from
// Error handling is VERY hacky, because of the lack of JS usage.
if($userDataSet->checkUserCredentials($username, $password)) {
$user->Authenticate($username, $password);
// Unset modal boolean to hide it's usage.
unset($_GET['modal']);
} else {
// Add error message and redirect to display modal
$view->loginError = "Invalid username or password.";
// Set modal boolean to header to allow modal to reappear
$queryParams = http_build_query(['modal' => 'true']);
header("Location: {$_SERVER['PHP_SELF']}?$queryParams");
// Handle OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Create service objects
$authService = new AuthService();
// Start session for CAPTCHA handling only
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Handle AJAX requests
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
header('Content-Type: application/json');
if (isset($_POST["action"])) {
switch ($_POST["action"]) {
case "login":
$username = $_POST["username"];
$password = hash("sha256", $_POST["password"]);
// Check if CAPTCHA is required (after 3 failed attempts)
$loginAttempts = isset($_SESSION['loginAttempts']) ? $_SESSION['loginAttempts'] : 0;
if ($loginAttempts >= 3) {
// Validate CAPTCHA
if (!isset($_POST["captchaInput"]) || !isset($_SESSION['captcha']) ||
$_POST["captchaInput"] !== $_SESSION['captcha']) {
// Generate new CAPTCHA for next attempt
$captcha = substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), 0, 5);
$_SESSION['captcha'] = $captcha;
echo json_encode([
"error" => "Incorrect CAPTCHA.",
"captcha" => $captcha
]);
exit;
}
// Clear CAPTCHA after successful validation
unset($_SESSION['captcha']);
}
// Authenticate user
$user = new User();
$token = $user->Authenticate($username, $password);
if ($token) {
// Reset login attempts on successful login
$_SESSION['loginAttempts'] = 0;
// Generate refresh token
$refreshToken = $authService->generateRefreshToken([
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
]);
echo json_encode([
'success' => true,
'token' => $token,
'refreshToken' => $refreshToken,
'user' => [
'id' => $user->getUserId(),
'username' => $user->getUsername(),
'accessLevel' => $user->getAccessLevel()
],
'redirect' => '/'
]);
} else {
// Increment login attempts
$_SESSION['loginAttempts'] = $loginAttempts + 1;
// If this failure triggers CAPTCHA, generate it
if ($_SESSION['loginAttempts'] >= 3) {
$captcha = substr(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"), 0, 5);
$_SESSION['captcha'] = $captcha;
echo json_encode([
"error" => "Invalid username or password.",
"captcha" => $captcha
]);
} else {
echo json_encode(["error" => "Invalid username or password."]);
}
}
break;
case "logout":
// Clear session (only used for CAPTCHA)
session_unset();
session_destroy();
echo json_encode([
"success" => true,
"redirect" => '/'
]);
break;
case "refresh":
// Validate refresh token
if (!isset($_POST['refreshToken'])) {
echo json_encode([
"valid" => false,
"error" => "No refresh token provided"
]);
exit;
}
$refreshToken = $_POST['refreshToken'];
$newToken = $authService->refreshToken($refreshToken);
if ($newToken) {
echo json_encode([
"valid" => true,
"token" => $newToken
]);
} else {
echo json_encode([
"valid" => false,
"error" => "Invalid or expired refresh token"
]);
}
break;
case "validate":
// Validate JWT token using the simplified approach
$auth = User::checkAuth(false);
if ($auth) {
echo json_encode([
"valid" => true,
"user" => [
"id" => $auth['uid'],
"username" => $auth['username'],
"accessLevel" => $auth['accessLevel']
]
]);
} else {
echo json_encode([
"valid" => false
]);
}
break;
}
exit;
}
}
if(isset($_POST['closeButton'])) {
unset($_GET['modal']);
}
if (isset($_POST["logoutButton"]))
{
$user->logout();
}
// for login errors; show login modal until captcha solved
if (isset($_GET['modal']) && $_GET['modal'] === 'true') {
$view->loginError = $view->loginError ?? "Please solve the Captcha and try again.";
}

View File

@@ -1,112 +0,0 @@
<?php
require_once('Models/FacilityDataSet.php');
require_once("Models/Paginator.php");
// Default Filters
$filters = [
'category' => $_GET['category'] ?? '1', // Default category
'term' => $_GET['term'] ?? '', // Default term
'sort' => $_GET['sort'] ?? '1', // Default sort
'dir' => $_GET['dir'] ?? 'asc', // Default direction
'page' => $_GET['page'] ?? 0 // Default to first page
];
// If no query parameters exist (initial page load), redirect to set default ones
if (empty($_GET)) {
redirectWithFilters($filters);
}
// Set row limit
$rowLimit = 7;
// create dataset object
$facilityDataSet = new FacilityDataSet();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
/**
* Unfortunately, ZERO time to fix this, too complex.
*/
if(isset($_POST['updateButton'])) {
$data = [
'id' => $_POST['idUpdate'],
'title' => $_POST['titlUpdate'],
'category' => $_POST['cateUpdate'],
'description' => $_POST['descUpdate'],
'houseNumber' => $_POST['hnumUpdate'],
'streetName' => $_POST['strtUpdate'],
'county' => $_POST['cntyUpdate'],
'town' => $_POST['townUpdate'],
'postcode' => $_POST['postUpdate'],
'lng' => $_POST['lngUpdate'],
'lat' => $_POST['latUpdate'],
'contributor' => $_POST['contUpdate'],
];
$facilityDataSet->addFacility($data);
}
if(isset($_POST['createButton'])) {
$data = [
'title' => $_POST['titlCreate'],
'category' => $_POST['cateCreate'],
'description' => $_POST['descCreate'],
'houseNumber' => $_POST['hnumCreate'],
'streetName' => $_POST['strtCreate'],
'county' => $_POST['cntyCreate'],
'town' => $_POST['townCreate'],
'postcode' => $_POST['postCreate'],
'contributor' => $_POST['contCreate'],
];
$facilityDataSet->addFacility($data);
}
// passes id to delete facility
if (isset($_POST['deleteButton'])) {
$facilityDataSet->deleteFacility($_POST['id']);
}
// Check if filters/sorting changed
$filtersChanged = (
$filters['category'] !== ($_POST['filterCat'] ?? $filters['category']) ||
$filters['term'] !== ($_POST['filter'] ?? $filters['term']) ||
$filters['sort'] !== ($_POST['sort'] ?? $filters['sort']) ||
$filters['dir'] !== ($_POST['dir'] ?? $filters['dir'])
);
// load from post if exists and sanitise, otherwise use defaults
$filters['category'] = filter_input(INPUT_POST, 'filterCat', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? $filters['category'];
$filters['term'] = filter_input(INPUT_POST, 'filter', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? $filters['term'];
$filters['sort'] = filter_input(INPUT_POST, 'sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? $filters['sort'];
$filters['dir'] = filter_input(INPUT_POST, 'dir', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? $filters['dir'];
// Reset page if filters changed
$filters['page'] = $filtersChanged ? 0 : $_POST['paginationButton'] ?? $filters['page'];
redirectWithFilters($filters);
}
// fetch page data from database
$view->allPageData = $facilityDataSet->fetchAll(
['category' => $filters['category'], 'term' => $filters['term']],
['sort' => $filters['sort'], 'dir' => $filters['dir']]
);
// set total facility count to view
$view->totalResults = $view->allPageData['count'];
// create paginator object
$view->paginator = new Paginator($rowLimit, $view->allPageData);
// assign page number to view
$view->pageNumber = $view->paginator->getPageFromUri();
// get current page
$view->pageData = $view->paginator->getPage($view->pageNumber);
// Send result count to view in format "showing x of y results"
$view->dbMessage = $view->paginator->countPageResults($view->pageNumber) == 0
? "No results"
: "Showing " . $view->paginator->countPageResults($view->pageNumber) . " of " . $view->totalResults . " result(s)";
// Redirect function, adds header parameters
function redirectWithFilters($filters) {
// Ensure no unintended keys are passed
$allowedKeys = ['category', 'term', 'sort', 'dir', 'page'];
$filters = array_filter($filters, function($key) use ($allowedKeys) {
return in_array($key, $allowedKeys);
}, ARRAY_FILTER_USE_KEY);
$queryString = http_build_query($filters);
header("Location: ?" . $queryString);
exit;
}

Some files were not shown because too many files have changed in this diff Show More