Compare commits
4 Commits
ab06cc8afd
...
cbafe85694
| Author | SHA1 | Date | |
|---|---|---|---|
| cbafe85694 | |||
| 4f06b1fe50 | |||
| c1f8c00728 | |||
| a10e6329fe |
226
package-lock.json
generated
226
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "ocean-cleanup",
|
||||
"name": "litter-scouts",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ocean-cleanup",
|
||||
"name": "litter-scouts",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.55.0",
|
||||
@@ -32,6 +32,7 @@
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
@@ -3392,6 +3393,16 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@@ -3744,6 +3755,21 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -3797,6 +3823,47 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -4178,6 +4245,13 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
@@ -4875,6 +4949,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -5399,6 +5483,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-generator-function": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||
@@ -6801,6 +6895,16 @@
|
||||
"regjsparser": "bin/parser"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -6890,6 +6994,16 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||
@@ -7070,6 +7184,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@@ -7226,6 +7353,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string.prototype.matchall": {
|
||||
"version": "4.0.12",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
|
||||
@@ -7328,6 +7470,19 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
|
||||
@@ -7492,6 +7647,16 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -8388,6 +8553,24 @@
|
||||
"workbox-core": "7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -8395,6 +8578,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
@@ -8402,6 +8595,35 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\" --names \"frontend,backend\" --prefix-colors \"cyan,magenta\"",
|
||||
"dev:frontend": "vite",
|
||||
"dev:backend": "cd server && npm run dev",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add equipment fields to events table
|
||||
ALTER TABLE `events` ADD COLUMN `equipment_provided` BOOLEAN DEFAULT false AFTER `photos`;
|
||||
ALTER TABLE `events` ADD COLUMN `required_items` JSON AFTER `equipment_provided`;
|
||||
@@ -83,6 +83,8 @@ model Event {
|
||||
participantCount Int? @default(0) @map("participant_count")
|
||||
litterCollected Decimal? @map("litter_collected") @db.Decimal(10, 2)
|
||||
photos Json?
|
||||
equipmentProvided Boolean? @default(false) @map("equipment_provided")
|
||||
requiredItems Json? @map("required_items")
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
updatedAt DateTime? @default(now()) @updatedAt @map("updated_at") @db.Timestamp(0)
|
||||
registrations EventRegistration[]
|
||||
|
||||
@@ -109,7 +109,7 @@ export const eventController = {
|
||||
// Find or create user in database
|
||||
const userId = await findOrCreateUser(clerkId);
|
||||
|
||||
const { title, description, latitude, longitude, locationName, scheduledDate, duration } = req.body;
|
||||
const { title, description, latitude, longitude, locationName, scheduledDate, duration, equipmentProvided, requiredItems } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!title || !description || !latitude || !longitude || !locationName || !scheduledDate || !duration) {
|
||||
@@ -129,6 +129,8 @@ export const eventController = {
|
||||
locationName,
|
||||
scheduledDate: new Date(scheduledDate),
|
||||
duration: parseInt(duration),
|
||||
equipmentProvided: equipmentProvided !== undefined ? Boolean(equipmentProvided) : false,
|
||||
requiredItems: Array.isArray(requiredItems) ? requiredItems : [],
|
||||
};
|
||||
|
||||
const event = await eventService.createEvent(eventData);
|
||||
|
||||
@@ -21,6 +21,8 @@ interface CreateEventData {
|
||||
locationName: string;
|
||||
scheduledDate: Date;
|
||||
duration: number;
|
||||
equipmentProvided?: boolean;
|
||||
requiredItems?: string[];
|
||||
}
|
||||
|
||||
interface UpdateEventData {
|
||||
@@ -31,6 +33,8 @@ interface UpdateEventData {
|
||||
status?: 'upcoming' | 'completed' | 'cancelled';
|
||||
litterCollected?: number;
|
||||
photos?: string[];
|
||||
equipmentProvided?: boolean;
|
||||
requiredItems?: string[];
|
||||
}
|
||||
|
||||
export const eventService = {
|
||||
@@ -93,6 +97,8 @@ export const eventService = {
|
||||
participantCount: event._count.registrations,
|
||||
litterCollected: event.litterCollected ? parseFloat(event.litterCollected.toString()) : undefined,
|
||||
photos: event.photos ? (Array.isArray(event.photos) ? event.photos : []) : undefined,
|
||||
equipmentProvided: (event as any).equipmentProvided || false,
|
||||
requiredItems: (event as any).requiredItems ? (Array.isArray((event as any).requiredItems) ? (event as any).requiredItems : []) : [],
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: event.updatedAt,
|
||||
}));
|
||||
@@ -137,6 +143,8 @@ export const eventService = {
|
||||
participantCount: event._count.registrations,
|
||||
litterCollected: event.litterCollected ? parseFloat(event.litterCollected.toString()) : undefined,
|
||||
photos: event.photos ? (Array.isArray(event.photos) ? event.photos : []) : undefined,
|
||||
equipmentProvided: (event as any).equipmentProvided || false,
|
||||
requiredItems: (event as any).requiredItems ? (Array.isArray((event as any).requiredItems) ? (event as any).requiredItems : []) : [],
|
||||
createdAt: event.createdAt,
|
||||
updatedAt: event.updatedAt,
|
||||
};
|
||||
@@ -145,16 +153,18 @@ export const eventService = {
|
||||
async createEvent(data: CreateEventData) {
|
||||
// Create point geometry for spatial queries
|
||||
const locationPoint = `POINT(${data.longitude} ${data.latitude})`;
|
||||
const equipmentProvided = data.equipmentProvided !== undefined ? data.equipmentProvided : false;
|
||||
const requiredItems = data.requiredItems && data.requiredItems.length > 0 ? JSON.stringify(data.requiredItems) : null;
|
||||
|
||||
const event = await prisma.$executeRaw`
|
||||
INSERT INTO events (
|
||||
id, organizer_id, title, description, location, latitude, longitude,
|
||||
location_name, scheduled_date, duration, status, participant_count
|
||||
location_name, scheduled_date, duration, status, participant_count, equipment_provided, required_items
|
||||
) VALUES (
|
||||
UUID(), ${data.organizerId}, ${data.title}, ${data.description},
|
||||
ST_GeomFromText(${locationPoint}, 4326),
|
||||
${data.latitude}, ${data.longitude}, ${data.locationName},
|
||||
${data.scheduledDate}, ${data.duration}, 'upcoming', 0
|
||||
${data.scheduledDate}, ${data.duration}, 'upcoming', 0, ${equipmentProvided}, ${requiredItems}
|
||||
)
|
||||
`;
|
||||
|
||||
@@ -195,6 +205,8 @@ export const eventService = {
|
||||
participantCount: createdEvent._count.registrations,
|
||||
litterCollected: createdEvent.litterCollected ? parseFloat(createdEvent.litterCollected.toString()) : undefined,
|
||||
photos: createdEvent.photos ? (Array.isArray(createdEvent.photos) ? createdEvent.photos : []) : undefined,
|
||||
equipmentProvided: (createdEvent as any).equipmentProvided || false,
|
||||
requiredItems: (createdEvent as any).requiredItems ? (Array.isArray((createdEvent as any).requiredItems) ? (createdEvent as any).requiredItems : []) : [],
|
||||
createdAt: createdEvent.createdAt,
|
||||
updatedAt: createdEvent.updatedAt,
|
||||
};
|
||||
|
||||
@@ -186,6 +186,54 @@ export function EventDetailModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Equipment Information */}
|
||||
<div className="flex items-start">
|
||||
<svg className="h-6 w-6 text-gray-400 mr-3 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
||||
/>
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900">Equipment</p>
|
||||
{event.equipmentProvided ? (
|
||||
<div className="mt-1">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
✓ Equipment Provided
|
||||
</span>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
All necessary cleanup equipment will be provided by the organizer.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
|
||||
Bring Your Own
|
||||
</span>
|
||||
{event.requiredItems && event.requiredItems.length > 0 ? (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-600 mb-2">Please bring the following items:</p>
|
||||
<ul className="space-y-1">
|
||||
{event.requiredItems.map((item, index) => (
|
||||
<li key={index} className="text-sm text-gray-700 flex items-center">
|
||||
<span className="text-blue-600 mr-2">•</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
Participants should bring their own cleanup equipment.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<svg className="h-6 w-6 text-gray-400 mr-3 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
|
||||
@@ -25,6 +25,8 @@ interface EventFormData {
|
||||
scheduledDate: string;
|
||||
scheduledTime: string;
|
||||
duration: number;
|
||||
equipmentProvided: boolean;
|
||||
requiredItems: string[];
|
||||
}
|
||||
|
||||
interface EventFormProps {
|
||||
@@ -44,8 +46,12 @@ export function EventForm({ onSubmit, onCancel, initialData, isSubmitting = fals
|
||||
scheduledDate: initialData?.scheduledDate || '',
|
||||
scheduledTime: initialData?.scheduledTime || '',
|
||||
duration: initialData?.duration || 120,
|
||||
equipmentProvided: initialData?.equipmentProvided || false,
|
||||
requiredItems: initialData?.requiredItems || [],
|
||||
});
|
||||
|
||||
const [newItem, setNewItem] = useState('');
|
||||
|
||||
const [selectedLocation, setSelectedLocation] = useState<Coordinates | null>(
|
||||
initialData?.latitude && initialData?.longitude
|
||||
? { latitude: initialData.latitude, longitude: initialData.longitude }
|
||||
@@ -328,6 +334,130 @@ export function EventForm({ onSubmit, onCancel, initialData, isSubmitting = fals
|
||||
{errors.duration && <p className="mt-1 text-sm text-red-600">{errors.duration}</p>}
|
||||
</div>
|
||||
|
||||
{/* Equipment Section */}
|
||||
<div className="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Equipment</h3>
|
||||
|
||||
{/* Equipment Provided Toggle */}
|
||||
<div className="mb-4">
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.equipmentProvided}
|
||||
onChange={(e) => {
|
||||
setFormData(prev => ({ ...prev, equipmentProvided: e.target.checked }));
|
||||
// Clear required items if equipment is provided
|
||||
if (e.target.checked) {
|
||||
setFormData(prev => ({ ...prev, requiredItems: [] }));
|
||||
}
|
||||
}}
|
||||
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="ml-3 text-sm font-medium text-gray-700">
|
||||
Equipment will be provided by organizer
|
||||
</span>
|
||||
</label>
|
||||
<p className="mt-1 ml-8 text-sm text-gray-500">
|
||||
Check this if you'll supply all necessary cleanup equipment
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Required Items List (only shown if equipment not provided) */}
|
||||
{!formData.equipmentProvided && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Items Participants Need to Bring
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
Add items that participants should bring (e.g., gloves, litter picker, bags, coat)
|
||||
</p>
|
||||
|
||||
{/* Add Item Input */}
|
||||
<div className="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={newItem}
|
||||
onChange={(e) => setNewItem(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (newItem.trim() && !formData.requiredItems.includes(newItem.trim())) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
requiredItems: [...prev.requiredItems, newItem.trim()]
|
||||
}));
|
||||
setNewItem('');
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="e.g., Gloves, Litter picker, Bags"
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (newItem.trim() && !formData.requiredItems.includes(newItem.trim())) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
requiredItems: [...prev.requiredItems, newItem.trim()]
|
||||
}));
|
||||
setNewItem('');
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Required Items List */}
|
||||
{formData.requiredItems.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-700">Required Items:</p>
|
||||
<ul className="space-y-1">
|
||||
{formData.requiredItems.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-white px-3 py-2 rounded-lg border border-gray-200"
|
||||
>
|
||||
<span className="text-sm text-gray-700">• {item}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
requiredItems: prev.requiredItems.filter((_, i) => i !== index)
|
||||
}));
|
||||
}}
|
||||
className="text-red-600 hover:text-red-800 text-sm font-medium"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.requiredItems.length === 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<p className="text-sm text-blue-700">
|
||||
💡 Tip: Add common items like gloves, litter pickers, bags, or appropriate clothing
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.equipmentProvided && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<p className="text-sm text-green-700">
|
||||
✓ Participants will be informed that all equipment will be provided
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Location Name */}
|
||||
<div>
|
||||
<label htmlFor="locationName" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
|
||||
@@ -77,13 +77,15 @@ export function MapPage() {
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
setIsLoadingEvents(true);
|
||||
// Fetch upcoming and completed events to show on map
|
||||
// Fetch upcoming and completed events to show on map (exclude cancelled)
|
||||
const data = await eventService.getEvents();
|
||||
setEvents(data);
|
||||
// Filter out cancelled events
|
||||
const activeEvents = data.filter(event => event.status !== 'cancelled');
|
||||
setEvents(activeEvents);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch events:', error);
|
||||
// Fall back to sample data on error
|
||||
setEvents(sampleEvents);
|
||||
// Fall back to sample data on error (also filter cancelled)
|
||||
setEvents(sampleEvents.filter(event => event.status !== 'cancelled'));
|
||||
} finally {
|
||||
setIsLoadingEvents(false);
|
||||
}
|
||||
@@ -386,42 +388,6 @@ export function MapPage() {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch hotspots:', error);
|
||||
// Use sample hotspot data for demonstration
|
||||
setHotspots([
|
||||
{
|
||||
id: 'hotspot1',
|
||||
latitude: 53.3498,
|
||||
longitude: -6.2603,
|
||||
radius: 500,
|
||||
reportCount: 15,
|
||||
severityScore: 0.75,
|
||||
lastReportDate: new Date('2024-01-20'),
|
||||
calculatedAt: new Date('2024-01-21'),
|
||||
severity: 'high',
|
||||
},
|
||||
{
|
||||
id: 'hotspot2',
|
||||
latitude: 51.8969,
|
||||
longitude: -8.4863,
|
||||
radius: 450,
|
||||
reportCount: 8,
|
||||
severityScore: 0.45,
|
||||
lastReportDate: new Date('2024-01-19'),
|
||||
calculatedAt: new Date('2024-01-21'),
|
||||
severity: 'medium',
|
||||
},
|
||||
{
|
||||
id: 'hotspot3',
|
||||
latitude: 53.2707,
|
||||
longitude: -9.0568,
|
||||
radius: 600,
|
||||
reportCount: 22,
|
||||
severityScore: 0.88,
|
||||
lastReportDate: new Date('2024-01-21'),
|
||||
calculatedAt: new Date('2024-01-21'),
|
||||
severity: 'critical',
|
||||
},
|
||||
]);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingHotspots(false);
|
||||
|
||||
@@ -62,6 +62,8 @@ export interface CleanupEvent {
|
||||
participantCount: number;
|
||||
litterCollected?: number; // in kilograms
|
||||
photos?: string[];
|
||||
equipmentProvided?: boolean; // Whether equipment is provided by organizer
|
||||
requiredItems?: string[]; // List of items participants need to bring
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from 'path'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 5200,
|
||||
host: true, // Allow external access
|
||||
allowedHosts: [
|
||||
'localhost',
|
||||
|
||||
Reference in New Issue
Block a user