From 96f79fb67410c798f8474c60d9d74b90ce1aa007 Mon Sep 17 00:00:00 2001 From: 42 <33488710+FortyTwoFortyTwo@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:12:19 +0100 Subject: [PATCH 1/3] Add admin page to edit event --- deno.lock | 17 ++-- package-lock.json | 10 +-- package.json | 3 +- src/api/services/events.ts | 33 ++++++++ src/components/minor/FormInput.tsx | 7 +- src/config/router.tsx | 5 ++ src/hooks/useEventById.ts | 21 +++++ src/pages/EditEvent.tsx | 131 +++++++++++++++++++++++++++++ 8 files changed, 208 insertions(+), 19 deletions(-) create mode 100644 src/hooks/useEventById.ts create mode 100644 src/pages/EditEvent.tsx diff --git a/deno.lock b/deno.lock index 6e22d4f..0778bd9 100644 --- a/deno.lock +++ b/deno.lock @@ -21,8 +21,7 @@ "npm:tw-animate-css@^1.3.0": "1.3.3", "npm:typescript-eslint@^8.26.1": "8.32.0_eslint@9.26.0_typescript@5.7.3_@typescript-eslint+parser@8.32.0__eslint@9.26.0__typescript@5.7.3", "npm:typescript@~5.7.2": "5.7.3", - "npm:vite@^6.3.1": "6.3.5_picomatch@4.0.2", - "npm:zod@^3.25.48": "3.25.48" + "npm:vite@^6.3.1": "6.3.5_picomatch@4.0.2" }, "npm": { "@ampproject/remapping@2.3.0": { @@ -290,7 +289,7 @@ "express-rate-limit", "pkce-challenge", "raw-body", - "zod@3.24.4", + "zod", "zod-to-json-schema" ] }, @@ -1043,7 +1042,7 @@ "minimatch@3.1.2", "natural-compare", "optionator", - "zod@3.24.4" + "zod" ], "bin": true }, @@ -1946,14 +1945,11 @@ "zod-to-json-schema@3.24.5_zod@3.24.4": { "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", "dependencies": [ - "zod@3.24.4" + "zod" ] }, "zod@3.24.4": { "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==" - }, - "zod@3.25.48": { - "integrity": "sha512-0X1mz8FtgEIvaxGjdIImYpZEaZMrund9pGXm3M6vM7Reba0e2eI71KPjSCGXBfwKDPwPoywf6waUKc3/tFvX2Q==" } }, "workspace": { @@ -1964,6 +1960,7 @@ "npm:@types/react-dom@^19.0.4", "npm:@types/react@^19.0.10", "npm:@vitejs/plugin-react-swc@^3.8.0", + "npm:axios@^1.9.0", "npm:eslint-config-prettier@^10.1.2", "npm:eslint-plugin-prettier@^5.2.6", "npm:eslint-plugin-react-hooks@^5.2.0", @@ -1971,6 +1968,7 @@ "npm:eslint@^9.22.0", "npm:globals@16", "npm:lucide-react@0.511", + "npm:mongodb@^6.17.0", "npm:prettier@3.5.3", "npm:react-dom@19", "npm:react-router@^7.6.0", @@ -1979,8 +1977,7 @@ "npm:tw-animate-css@^1.3.0", "npm:typescript-eslint@^8.26.1", "npm:typescript@~5.7.2", - "npm:vite@^6.3.1", - "npm:zod@^3.25.48" + "npm:vite@^6.3.1" ] } } diff --git a/package-lock.json b/package-lock.json index aedf302..00fdfbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router": "^7.6.0", - "tailwindcss": "^4.1.6", - "zod": "^3.25.48" + "tailwindcss": "^4.1.6" }, "devDependencies": { "@eslint/js": "^9.22.0", @@ -4938,9 +4937,10 @@ } }, "node_modules/zod": { - "version": "3.25.48", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.48.tgz", - "integrity": "sha512-0X1mz8FtgEIvaxGjdIImYpZEaZMrund9pGXm3M6vM7Reba0e2eI71KPjSCGXBfwKDPwPoywf6waUKc3/tFvX2Q==", + "version": "3.25.56", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz", + "integrity": "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 5fdc49a..ff52c91 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router": "^7.6.0", - "tailwindcss": "^4.1.6", - "zod": "^3.25.48" + "tailwindcss": "^4.1.6" }, "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/src/api/services/events.ts b/src/api/services/events.ts index 95a2020..0690ea7 100644 --- a/src/api/services/events.ts +++ b/src/api/services/events.ts @@ -1,3 +1,8 @@ +import axios from 'axios'; +import api from '../api'; + +import { Event } from 'models/event.model'; + export const getEventByMode = async (modes: string[]) => { const query = new URLSearchParams({ mode: modes.join(',') }).toString(); @@ -8,3 +13,31 @@ export const getEventByMode = async (modes: string[]) => { } return response.json(); }; + +export const getEventById = async (id: string) => { + const response = await fetch(`http://localhost:3000/events/${id}`); + + if (!response.ok) { + throw new Error('Failed to fetch events'); + } + return response.json(); +}; + +export interface UpdateEventResponse { + message: string; +} + +export const updateEventById = async ( + id: string, + event: Event, +): Promise => { + try { + const { data } = await api.put('/events/' + id, event); + return data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 401) { + return null; + } + throw error; + } +}; diff --git a/src/components/minor/FormInput.tsx b/src/components/minor/FormInput.tsx index b064826..c98786b 100644 --- a/src/components/minor/FormInput.tsx +++ b/src/components/minor/FormInput.tsx @@ -1,15 +1,17 @@ -type InputType = 'text' | 'email' | 'password'; +type InputType = 'text' | 'email' | 'number' | 'password'; interface FormInputProps { name: string; label: string; + def?: string | number; type?: InputType; - error: boolean; + error?: boolean; } export default function FormInput({ name, label, + def = '', type = 'text', error = false, }: FormInputProps) { @@ -20,6 +22,7 @@ export default function FormInput({ type={type} id={name} name={name} + defaultValue={def} required className={`bg-[var(--color-input-bg)] text-[var(--color-text)] outline-none pl-1 border ${ error diff --git a/src/config/router.tsx b/src/config/router.tsx index 017bd57..70d89c3 100644 --- a/src/config/router.tsx +++ b/src/config/router.tsx @@ -7,6 +7,7 @@ import UserHome from '../pages/UserHome'; import UserProfile from '../pages/UserProfile'; import Error from '../pages/Error'; import SavedEvents from '../pages/SavedEvents'; +import EditEvent from '../pages/EditEvent'; const router = createBrowserRouter([ { @@ -33,6 +34,10 @@ const router = createBrowserRouter([ path: '/savedevents/:mode?', element: , }, + { + path: '/events/edit/:id', + element: , + }, { path: '/*', element: , diff --git a/src/hooks/useEventById.ts b/src/hooks/useEventById.ts new file mode 100644 index 0000000..d5adc31 --- /dev/null +++ b/src/hooks/useEventById.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from 'react'; +import { getEventById } from '../api/services/events'; +import { Event } from 'models/event.model'; + +export const useEventById = (id: string) => { + const [event, setEvent] = useState(); + + useEffect(() => { + const getEvent = async () => { + try { + const data = await getEventById(id); + setEvent(data); + } catch (error) { + new Error('Error setting event by id: ' + error); + } + }; + getEvent(); + }, [id]); + + return { event, setEvent }; +}; diff --git a/src/pages/EditEvent.tsx b/src/pages/EditEvent.tsx new file mode 100644 index 0000000..1c59622 --- /dev/null +++ b/src/pages/EditEvent.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { useParams } from 'react-router'; +import { updateEventById } from '../api/services/events.ts'; +import { useEventById } from '../hooks/useEventById.ts'; + +import '../styles/home.css'; +import NavBar from '../components/major/nav-bar'; +import FormInput from '../components/minor/FormInput'; + +import { Event } from 'models/event.model.ts'; + +export default function EditEvent() { + let { id } = useParams(); + if (id === undefined) id = ''; + + const eventId: string = id; + const { event } = useEventById(eventId); + const [formData, setFormData] = useState(null); + const [message, setMessage] = useState(''); + + if (!event) return; + + if (formData === null) { + const data: Event = { + mode: event.mode, + name: event.name, + description: event.description, + location: event.location, + date: event.date, + price: event.price, + distance: event.distance, + url: event.url, + }; + + setFormData(data); + } + + const handleChange = (e: React.FormEvent) => { + const { name, value } = e.target as HTMLInputElement; + setFormData((prevState) => + prevState ? { ...prevState, [name]: value } : null, + ); + }; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + if (!formData) return; + + const result = await updateEventById(eventId, formData); + if (result && result.message) setMessage(result.message); + } + + return ( +
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + +
+ +
+
+
+
+ {message && ( +
+ {message} +
+ )} +
+
+
+ ); +} From 9689ab5b8e97cb24ca3fa3f69471ad9b66552a3e Mon Sep 17 00:00:00 2001 From: 42 <33488710+FortyTwoFortyTwo@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:41:03 +0100 Subject: [PATCH 2/3] Add button to edit page in event card --- shared | 2 +- src/components/major/events.tsx | 27 +++++++++++++++++++++--- src/components/major/eventsContainer.tsx | 17 ++++----------- src/hooks/useEvents.ts | 6 +++--- src/utils/filterEvents.ts | 6 +++--- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/shared b/shared index 1592042..e33f2b8 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit 15920427aa1a0dea6d5d636f7b1614783bd1d974 +Subproject commit e33f2b8fb41afa8dbeab22570c0039cb53c1af45 diff --git a/src/components/major/events.tsx b/src/components/major/events.tsx index f2747ee..5d7416d 100644 --- a/src/components/major/events.tsx +++ b/src/components/major/events.tsx @@ -1,10 +1,31 @@ //component for events data -import { Event } from 'models/event.model.ts'; +import { Link } from 'react-router'; +import { Wrench } from 'lucide-react'; + +import { useAuth } from '../../auth/useAuth'; +import { FullEvent } from 'models/event.model.ts'; + +export default function Events(info: FullEvent) { + const { user } = useAuth(); + const admin = user && user.role === 'admin'; -export default function Events(info: Event) { return (
-

{info.name}

+
+

{info.name}

+ {admin ? ( +
+ + + +
+ ) : ( + '' + )} +

{info.location}

{info.distance}km away

{new Date(info.date).toLocaleDateString()}

diff --git a/src/components/major/eventsContainer.tsx b/src/components/major/eventsContainer.tsx index 43957dc..1625ade 100644 --- a/src/components/major/eventsContainer.tsx +++ b/src/components/major/eventsContainer.tsx @@ -1,24 +1,15 @@ import Events from './events'; -import { Event } from 'models/event.model'; +import { FullEvent } from 'models/event.model'; type Props = { - events: Event[]; + events: FullEvent[]; }; const EventsContainer = ({ events }: Props) => { return (
- {events.map((event) => ( - + {events.map((event: FullEvent) => ( + ))}
); diff --git a/src/hooks/useEvents.ts b/src/hooks/useEvents.ts index 0f088c7..9188bb9 100644 --- a/src/hooks/useEvents.ts +++ b/src/hooks/useEvents.ts @@ -2,11 +2,11 @@ import { useState, useEffect } from 'react'; import { FiltersState } from '../components/major/side-bar/types'; import { getEventByMode } from '../api/services/events'; import { filterEvents } from '../utils/filterEvents'; -import { Event } from 'models/event.model'; +import { FullEvent } from 'models/event.model'; export const useEvents = (filters: FiltersState) => { - const [rawEvents, setRawEvents] = useState([]); - const [filteredEvents, setFilteredEvents] = useState([]); + const [rawEvents, setRawEvents] = useState([]); + const [filteredEvents, setFilteredEvents] = useState([]); useEffect(() => { const getEvents = async () => { diff --git a/src/utils/filterEvents.ts b/src/utils/filterEvents.ts index 1d61ffb..2e4606a 100644 --- a/src/utils/filterEvents.ts +++ b/src/utils/filterEvents.ts @@ -1,8 +1,8 @@ -import { Event } from 'models/event.model'; +import { FullEvent } from 'models/event.model'; import { FiltersState } from '../components/major/side-bar/types'; -export const filterEvents = (events: Event[], filters: FiltersState) => { - return events.filter((event: Event) => { +export const filterEvents = (events: FullEvent[], filters: FiltersState) => { + return events.filter((event: FullEvent) => { const searchMatch = filters.search === '' || event.name From bc0c9fabb04c2cf1156b5b4d10cef83db22dfd19 Mon Sep 17 00:00:00 2001 From: 42 <33488710+FortyTwoFortyTwo@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:54:48 +0100 Subject: [PATCH 3/3] Add button to delete event --- src/api/services/events.ts | 18 +++++++++ src/components/major/events.tsx | 66 +++++++++++++++++++++++++++------ src/styles/home.css | 5 +++ 3 files changed, 78 insertions(+), 11 deletions(-) diff --git a/src/api/services/events.ts b/src/api/services/events.ts index 0690ea7..d742bc3 100644 --- a/src/api/services/events.ts +++ b/src/api/services/events.ts @@ -41,3 +41,21 @@ export const updateEventById = async ( throw error; } }; + +export interface DeleteEventResponse { + message: string; +} + +export const deleteEventById = async ( + id: string, +): Promise => { + try { + const { data } = await api.delete('/events/' + id); + return data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 401) { + return null; + } + throw error; + } +}; diff --git a/src/components/major/events.tsx b/src/components/major/events.tsx index 5d7416d..6466284 100644 --- a/src/components/major/events.tsx +++ b/src/components/major/events.tsx @@ -1,37 +1,81 @@ //component for events data -import { Link } from 'react-router'; -import { Wrench } from 'lucide-react'; +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router'; +import { Wrench, Trash2 } from 'lucide-react'; import { useAuth } from '../../auth/useAuth'; +import { deleteEventById } from '../../api/services/events.ts'; import { FullEvent } from 'models/event.model.ts'; +import DirectButton from '../minor/DirectButton'; + export default function Events(info: FullEvent) { const { user } = useAuth(); const admin = user && user.role === 'admin'; + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + + const handleDelete = async () => { + // TODO handle response from API for fails + if (info._id !== undefined) { + await deleteEventById(info._id.toString()); + + navigate(0); // refreshes the current page were in, as we dont want to show deleted event + } + + setOpen(false); + }; + return (
-

{info.name}

- {admin ? ( -
+
+

{info.name}

+

{info.location}

+

{info.distance}km away

+

{new Date(info.date).toLocaleDateString()}

+
+ {admin && ( +
+ +
- ) : ( - '' )}
-

{info.location}

-

{info.distance}km away

-

{new Date(info.date).toLocaleDateString()}

{info.description}

Price: {info.price}

More info can be found here - {info.url}

+ + {open && ( +
setOpen(false)} + className="fixed inset-0 bg-black/50 flex justify-center items-center" + > +
+

+ Are you sure you want to delete {info.name}? +

+ +
+ + + setOpen(false)} text="No" /> +
+
+
+ )}
); } diff --git a/src/styles/home.css b/src/styles/home.css index f8705cc..7972395 100644 --- a/src/styles/home.css +++ b/src/styles/home.css @@ -2,3 +2,8 @@ display: flex; justify-content: space-around; } + +.popup { + background: var(--color-background); + border-radius: 1rem; +} \ No newline at end of file