buidlingreactUI #27

Closed
madeleinema wants to merge 2 commits from buidlingreactUI into master
11 changed files with 960 additions and 0 deletions

24
frontend/README.md Normal file
View file

@ -0,0 +1,24 @@
# Localist Frontend (React + Vite)
This directory contains the frontend application for Localist built with React + Vite.
It provides an event creation form with an embedded Leaflet map and a search bar (leaflet-geosearch). The form uses react-hook-form with a Zod schema to validate and transform outgoing payloads (the schema nests flat latitude/longitude into a `location` object before sending).
Quick links
- Source: `src/`
- Main component: `src/components/eventForm.jsx`
- Validation/transform schema: `src/schemas/eventPostSchema.js`
- API client: `src/services/eventService.js`
- Vite config: `vite.config.js`
Prerequisites
- Node.js (16+ recommended)
- npm or yarn
- Django backend running locally at http://127.0.0.1:8000 if you want the API to respond (the Vite config proxies `/api` to that address by default)
Install dependencies
```bash
cd localist/frontend
npm install
# or
yarn

41
frontend/package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^5.4.0",
"@mui/material": "^9.1.1",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"leaflet-geosearch": "^4.4.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-hook-form": "^7.80.0",
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.18.0",
"zod": "^4.4.3"
},
"devDependencies": {
Review

why are dev dependencies different from prod dependencies?

why are dev dependencies different from prod dependencies?
"@eslint/js": "^10.0.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"postcss": "^8.5.15",
"tailwindcss": "^4.3.1",
"vite": "^8.0.12"
}
}

184
frontend/src/App.css Normal file
View file

@ -0,0 +1,184 @@
.counter {
Review

is it react standard for the file to be uppercase App.css? having an uppercase letter in file is weird and i would only do it if it was required by react

is it react standard for the file to be uppercase App.css? having an uppercase letter in file is weird and i would only do it if it was required by react
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
Review

what actually is vite, why is it required

what actually is vite, why is it required
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
Review

are these better as ids or classes?

are these better as ids or classes?
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
Review

i feel like a lot of these names are ambiguous. like next steps what, docs what? I imagine we will have a documentation page.

i feel like a lot of these names are ambiguous. like next steps what, docs what? I imagine we will have a documentation page.
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
Review

this implies we will have multiple link styles... i would take a look at a lot of your nested css and see if there are things that could be put at the global level, why do we need a special link here, it is just going to lead to an inconsistent UI

this implies we will have multiple link styles... i would take a look at a lot of your nested css and see if there are things that could be put at the global level, why do we need a special link here, it is just going to lead to an inconsistent UI
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

62
frontend/src/App.jsx Normal file
View file

@ -0,0 +1,62 @@
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
Review

all imports should at least be in some sort of alphabetical order. idk how best to do that in react tho

all imports should at least be in some sort of alphabetical order. idk how best to do that in react tho
import { useMapEvents } from 'react-leaflet';
import { Routes, Route, Link } from "react-router-dom";
import EventForm from './components/eventForm.jsx'; // <- add this import
Review

what is a .jsx file anyway?

what is a .jsx file anyway?
function ClickHandler() {
Review

this function is just for debugging, probably should not be in master

this function is just for debugging, probably should not be in master
useMapEvents({
click(e) {
console.log(
e.latlng.lat,
e.latlng.lng
);
},
});
return null;
}
// src/App.jsx
function Home() {
madeleinema marked this conversation as resolved
Review

the lower function has export default function but this one just has function, why?

what do these functions even do?

the lower function has export default function but this one just has function, why? what do these functions even do?
return (
<div>
<h1>Home</h1>
<p><Link to="/createevent">Create an event</Link></p>
</div>
);
}
export default function App() {
return (
<div>
<Routes>
madeleinema marked this conversation as resolved
Review

this has routes, the other doesn't, why?

this has routes, the other doesn't, why?
<Route path="/" element={<Home />} />
<Route path="/createevent" element={<EventForm />} />
</Routes>
</div>
);
}
// export default function App() {
madeleinema marked this conversation as resolved
Review

delete commented code

delete commented code
// return (
// <div style={{ height: '100vh' }}>
// <MapContainer
// center={[40.437, -74.199]} // Keyport area
// zoom={13}
// style={{ height: '100%', width: '100%' }}
// >
// <TileLayer
// attribution="&copy; OpenStreetMap contributors"
// url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
// />
// <ClickHandler />
//
// <Marker position={[40.437, -74.199]}>
// <Popup>
// Hello from Leaflet!
// </Popup>
// </Marker>
// </MapContainer>
// </div>
// );
// }

View file

@ -0,0 +1,124 @@
/* eventForm.css */
Review

damn okay. we have a lot of css in multiple files. you need to probably really create a single css file and inherit it in most cases, with only minor tweaks for each page.

damn okay. we have a lot of css in multiple files. you need to probably really create a single css file and inherit it in most cases, with only minor tweaks for each page.
.event-form {
max-width: 900px;
margin: 28px auto;
padding: 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.06);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
color: #111827;
}
.form-title {
margin: 0 0 16px;
font-size: 1.5rem;
font-weight: 600;
color: #111827;
}
.form-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
/* On wider screens use two columns */
@media (min-width: 700px) {
.form-grid {
grid-template-columns: 1fr 1fr;
gap: 14px;
}
}
/* Make these fields span both columns */
.full-width {
grid-column: 1 / -1;
}
.form-field {
display: flex;
flex-direction: column;
}
.form-field label {
font-size: 0.9rem;
margin-bottom: 6px;
color: #374151;
}
.form-field input[type="text"],
.form-field input[type="url"],
.form-field input[type="email"],
.form-field input[type="tel"],
.form-field input[type="number"],
.form-field input[type="datetime-local"],
.form-field textarea,
.form-field select {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 10px;
font-size: 0.95rem;
outline: none;
transition: box-shadow .12s ease, border-color .12s ease;
}
.form-field textarea {
min-height: 110px;
resize: vertical;
}
.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus {
border-color: #60a5fa;
box-shadow: 0 4px 10px rgba(96,165,250,0.12);
}
/* Checkbox row styling */
.checkbox-row {
display: flex;
align-items: center;
gap: 8px;
padding-top: 6px;
}
/* Actions */
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 14px;
}
.btn {
background: linear-gradient(180deg, #2563eb, #1e40af);
color: #fff;
border: none;
padding: 10px 16px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
box-shadow: 0 6px 18px rgba(30,64,175,0.12);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(30,64,175,0.14);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 6px 18px rgba(30,64,175,0.12);
}
/* Small screens tweak */
@media (max-width: 420px) {
.event-form {
padding: 14px;
}
}
.leaflet-control-attribution .leaflet-attrib-flag {
display: none;
}

View file

@ -0,0 +1,257 @@
/*
EventForm - create/edit event UI with Leaflet map + leaflet-geosearch.
- Uses react-hook-form + Zod resolver for validation/coercion
- Map provides a search bar (OpenStreetMapProvider) and click-to-place marker
- When a place is chosen, address + coordinates are written into the form values
- Coordinates are submitted as flat fields; the schema transforms them into `location`
*/
import React, {useEffect, useState} from "react";
Review

organize imports better

organize imports better
import {useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import EventService from "../services/eventService";
import {EventPostSchema} from "../schemas/eventPostSchema";
// Leaflet icon compatibility helper (handles bundler issues)
import 'leaflet-defaulticon-compatibility';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css';
// Leaflet / React-Leaflet
import {MapContainer, TileLayer, Marker, useMap, useMapEvents} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "./eventForm.css"
// GeoSearch
import {GeoSearchControl, OpenStreetMapProvider} from "leaflet-geosearch";
import "leaflet-geosearch/dist/geosearch.css";
/* Fix leaflet icon paths for many bundlers (Vite/CRA/Webpack) */
import markerUrl from "leaflet/dist/images/marker-icon.png";
import markerRetinaUrl from "leaflet/dist/images/marker-icon-2x.png";
import markerShadowUrl from "leaflet/dist/images/marker-shadow.png";
L.Icon.Default.mergeOptions({
madeleinema marked this conversation as resolved
Review

what does this do?

what does this do?
iconRetinaUrl: markerRetinaUrl,
iconUrl: markerUrl,
shadowUrl: markerShadowUrl,
});
export default function EventForm() {
const eventService = new EventService();
const [serverError, setServerError] = useState(null);
const {
register,
handleSubmit,
formState: {errors, isSubmitting, isDirty},
reset,
setValue,
watch,
} = useForm({
resolver: zodResolver(EventPostSchema),
defaultValues: {
Review

this is good for dev, but we need to remove them for prod.

this is good for dev, but we need to remove them for prod.
name: "Madeleine's birthday",
description: "test",
url: "http://localhost:8080",
address: "31 Fleetwood",
status: "scheduled",
price: "10",
require_rsvp: false,
start_time: "",
end_time: "",
rain_date: "",
email: "madeleienma07@gmail.com",
phone_number: "8482189789",
latitude: "", // hidden field for latitude
Review

remove ai slop comments

remove ai slop comments
longitude: "", // hidden field for longitude
},
});
// Status options for the select dropdown
const STATUS_OPTIONS = [
{value: "scheduled", label: "Scheduled"},
{value: "completed", label: "Completed"},
{value: "canceled", label: "Canceled"},
];
// submit
const onSubmit = async (data) => {
setServerError(null);
try {
// Schema handles transformation - data is already nested as { location: { latitude, longitude } }
await eventService.createEvent(data);
alert("Your event has been created successfully");
Review

i dont love alerts for users. better to do message toast like django

i dont love alerts for users. better to do message toast like django
reset();
} catch (err) {
console.error(err);
setServerError(err.body ?? err.message ?? "Network error");
alert("Failed to save: " + (err.status || "") + " " + JSON.stringify(err.body || err.message));
}
};
// Reading lat/lng so the marker can follow them
const latitude = watch("latitude");
const longitude = watch("longitude");
// Map child that adds the GeoSearch control and listens for results
function GeoSearchControlComponent() {
const map = useMap();
useEffect(() => {
const provider = new OpenStreetMapProvider();
const searchControl = new GeoSearchControl({
provider,
style: "bar",
showMarker: false,
retainZoomLevel: false,
autoClose: true,
keepResult: false,
});
map.addControl(searchControl);
const onShowLocation = (e) => {
const {x: lon, y: lat, label} = e.location;
// Set all three fields from the search result
setValue("address", label, {shouldDirty: true, shouldValidate: true});
setValue("latitude", lat, {shouldDirty: true, shouldValidate: true});
setValue("longitude", lon, {shouldDirty: true, shouldValidate: true});
map.setView([lat, lon], Math.max(map.getZoom(), 13));
};
map.on("geosearch/showlocation", onShowLocation);
return () => {
map.removeControl(searchControl);
map.off("geosearch/showlocation", onShowLocation);
};
}, [map, setValue]);
return null;
}
// Map child that listens for click events to set coordinates
function ClickToSetMarker() {
useMapEvents({
click(e) {
setValue("latitude", e.latlng.lat, {shouldDirty: true, shouldValidate: true});
setValue("longitude", e.latlng.lng, {shouldDirty: true, shouldValidate: true});
},
});
return null;
}
// Default center if no coordinates set
const defaultCenter = [40.35, -74.31];
// parse lat/lng from watched values
const markerPosition = (latitude && longitude) ? [parseFloat(latitude), parseFloat(longitude)] : null;
return (
Review

why is this not in app.jsx then? im not saying to do that, but this is inconsistent.

why is this not in app.jsx then? im not saying to do that, but this is inconsistent.
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div>
<label>Name
<input {...register("name")} placeholder="Name"/>
{errors.name && <div style={{color: "red"}}>{errors.name.message}</div>}
</label>
</div>
<div>
<label>Description
<textarea {...register("description")} placeholder="Description"/>
{errors.description && <div style={{color: "red"}}>{errors.description.message}</div>}
</label>
</div>
<div>
<input {...register("url")} placeholder="URL" type="url"/>
{errors.url && <div style={{color: "red"}}>{errors.url.message}</div>}
</div>
{/* Map and search control */}
<div style={{marginTop: 8}}>
Review

you can probably abstract the map html into its own component, because it is going to have to be reused again

you can probably abstract the map html into its own component, because it is going to have to be reused again
<div style={{height: 320, width: "100%"}}>
<MapContainer
center={markerPosition || defaultCenter}
zoom={markerPosition ? 13 : 11}
style={{height: "100%", width: "100%"}}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"/>
<GeoSearchControlComponent/>
<ClickToSetMarker/>
{markerPosition && <Marker position={markerPosition}/>}
</MapContainer>
</div>
<div style={{fontSize: 12, color: "#666", marginTop: 6}}>
Tip: search an address on the map bar or click the map to set location.
</div>
</div>
<div>
{/* Address input - if you want you can also update address automatically from search result */}
madeleinema marked this conversation as resolved
Review

ai slop comment

ai slop comment
<input {...register("address")} placeholder="Address"/>
{errors.address && <div style={{color: "red"}}>{errors.address.message}</div>}
</div>
<div>
<label htmlFor="status">Status</label>
<select id="status" {...register("status")}>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{errors.status && <div style={{color: "red"}}>{errors.status.message}</div>}
</div>
<div>
<input {...register("price")} placeholder="Price USD" type="number" step="1.0"/>
{errors.price && <div style={{color: "red"}}>{errors.price.message}</div>}
</div>
<div>
<label>
<input {...register("require_rsvp")} type="checkbox"/>
Require RSVP
</label>
</div>
<div>
<label>Start time
<input {...register("start_time")} type="datetime-local"/>
</label>
{errors.start_time && <div style={{color: "red"}}>{errors.start_time.message}</div>}
</div>
<div>
<label>End time
<input {...register("end_time")} type="datetime-local"/>
</label>
{errors.end_time && <div style={{color: "red"}}>{errors.end_time.message}</div>}
</div>
<div>
<label>Rain date
<input {...register("rain_date")} type="datetime-local"/>
</label>
{errors.rain_date && <div style={{color: "red"}}>{errors.rain_date.message}</div>}
</div>
<div>
<input {...register("email")} placeholder="Email" type="email"/>
{errors.email && <div style={{color: "red"}}>{errors.email.message}</div>}
</div>
<div>
<input {...register("phone_number")} placeholder="Phone number" type="tel"/>
{errors.phone_number && <div style={{color: "red"}}>{errors.phone_number.message}</div>}
</div>
{/* Hidden fields for coordinates (these will be submitted) */}
<input type="hidden" {...register("latitude")} />
<input type="hidden" {...register("longitude")} />
<div style={{marginTop: 12}}>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Event"}
</button>
</div>
</form>
);
}

111
frontend/src/index.css Normal file
View file

@ -0,0 +1,111 @@
:root {
Review

damn, more css

damn, more css
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
Review

probably wana do prefers-color-scheme: system (idr if that is what it is tho)

probably wana do prefers-color-scheme: system (idr if that is what it is tho)
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}

16
frontend/src/main.jsx Normal file
View file

@ -0,0 +1,16 @@
import { StrictMode } from 'react'
import React from "react";
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import 'leaflet/dist/leaflet.css';
import { BrowserRouter } from "react-router-dom";
const root = createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

View file

@ -0,0 +1,85 @@
/*
Schema for outgoing POST body to /api/event
- Accepts form values (strings/numbers) and coerces where useful.
- Transforms flat latitude/longitude fields into a nested `location` object:
location: { latitude: number | null, longitude: number | null }
- Removes the flat latitude/longitude fields from the final payload.
*/
import {z} from "zod";
export const EventPostSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().min(5, "Description has to be more than 5 letters."),
madeleinema marked this conversation as resolved
Review

what is the first integer in min function mean.

what is the first integer in min function mean.
url: z.string().url().nullable().optional(),
address: z.string().min(5, "Address is required"),
status: z.enum(["scheduled", "completed", "canceled"]).optional(),
Review

seems like you have statuses written in two places, would be better if they were shared somehow

seems like you have statuses written in two places, would be better if they were shared somehow
// price: allow number or string or null; coerce to number or null
price: z
.union([z.string(), z.number(), z.null()])
.optional()
.transform((val) => {
if (val === undefined || val === null || val === "") return null;
const n = typeof val === "string" ? parseFloat(val) : val;
return Number.isFinite(n) ? n : null;
}),
require_rsvp: z.boolean().optional().default(false),
// start_time: optional, but if provided must be a valid Date and in the future.
start_time: z.preprocess((val) => {
if (val === "" || val === undefined || val === null) return null;
if (typeof val === "string") {
const d = new Date(val);
return isNaN(d.getTime()) ? val : d;
}
return val;
},
z.union([
z.date({invalid_type_error: "Start date must be a valid datetime"}),
z.null()
])
.refine((v) => v === null || v.getTime() > Date.now(), {
message: "Start date must be in the future",
})
)
.transform((val) => (val === null ? null : val.toISOString())),
end_time: z.union([z.string(), z.date(), z.null()])
.optional()
.transform((val) => {
if (!val) return null;
return val instanceof Date ? val.toISOString() : val;
}),
rain_date: z
.union([z.string(), z.date(), z.null()])
.optional()
.transform((val) => {
if (!val) return null;
return val instanceof Date ? val.toISOString() : val;
}),
email: z.string().email().nullable().optional(),
phone_number: z.string().nullable().optional(),
// Flat latitude and longitude fields for form handling
latitude: z.union([z.string(), z.number(), z.null()]).optional(),
longitude: z.union([z.string(), z.number(), z.null()]).optional(),
})
.transform((data) => {
// Transform flat latitude/longitude into nested location object
if (data.latitude || data.longitude) {
data.location = {
lat: data.latitude ? parseFloat(data.latitude) : null,
lng: data.longitude ? parseFloat(data.longitude) : null,
};
}
// Remove flat fields from payload
delete data.latitude;
delete data.longitude;
return data;
});

View file

@ -0,0 +1,46 @@
import {EventPostSchema} from "../schemas/eventPostSchema";
// Service responsible for sending event POST requests to the API.
// - Validates and coerces the outgoing payload using EventPostSchema
// - Logs validation errors (if any) and the final payload sent
// - Sends the request with CSRF token (if available) and returns parsed JSON response
const API_BASE = import.meta.env.VITE_API_BASE
function getCSRFToken() {
const name = "csrftoken";
const match = document.cookie.match(new RegExp('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)'));
return match ? match.pop() : null;
}
export default class EventService {
constructor(base = API_BASE) {
this.base = base;
}
async createEvent(rawPayload) {
// Validate and coerce the outgoing payload
const parsed = EventPostSchema.parse(rawPayload); // will throw if invalid
console.log("EventService sending payload:", JSON.stringify(rawPayload, null, 2)); // Log here
Review

ai slop comments and unnecessary console.log

ai slop comments and unnecessary console.log
const res = await fetch(`${this.base}/event`, {
method: "POST",
headers: {
"Content-Type": "application/json",
// X-CSRFToken if using django session auth in browser
...(getCSRFToken() ? {"X-CSRFToken": getCSRFToken()} : {}),
},
credentials: "same-origin", // change to 'include' if cross-site cookies
body: JSON.stringify(parsed),
});
if (!res.ok) {
const text = await res.text();
const err = new Error("Failed to create event");
err.status = res.status;
err.body = text;
throw err;
}
return res.json();
}
}

View file

@ -1,4 +1,8 @@
import sys
Review

why is this file in here?

why is this file in here?
from pathlib import Path
from config import DB_NAME, DB_USER, DB_HOST, DB_PASSWORD, DB_PORT, SECRET_KEY
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@ -34,6 +38,12 @@ INSTALLED_APPS = [
"api",
]
if sys.platform == "darwin":
GDAL_LIBRARY_PATH = "/opt/homebrew/opt/gdal/lib/libgdal.dylib"
GEOS_LIBRARY_PATH = "/opt/homebrew/opt/geos/lib/libgeos_c.dylib"
SECURE_REFERER_POLICY = "no-referrer-when-downgrade"
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",