diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..b2dc66f
--- /dev/null
+++ b/frontend/README.md
@@ -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
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..d7f5f84
--- /dev/null
+++ b/frontend/package.json
@@ -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": {
+ "@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"
+ }
+}
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..f90339d
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,184 @@
+.counter {
+ 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 {
+ 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 {
+ 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 {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 8px;
+ margin: 32px 0 0;
+
+ .logo {
+ height: 18px;
+ }
+
+ a {
+ 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);
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..eb4c07b
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,62 @@
+import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
+import { useMapEvents } from 'react-leaflet';
+import { Routes, Route, Link } from "react-router-dom";
+import EventForm from './components/eventForm.jsx'; // <- add this import
+
+function ClickHandler() {
+ useMapEvents({
+ click(e) {
+ console.log(
+ e.latlng.lat,
+ e.latlng.lng
+ );
+ },
+ });
+
+ return null;
+}
+
+// src/App.jsx
+function Home() {
+ return (
+
+
Home
+
Create an event
+
+ );
+}
+
+export default function App() {
+ return (
+
+
+ } />
+ } />
+
+
+ );
+}
+
+// export default function App() {
+// return (
+//
+//
+//
+//
+//
+//
+//
+// Hello from Leaflet!
+//
+//
+//
+//
+// );
+// }
\ No newline at end of file
diff --git a/frontend/src/components/eventForm.css b/frontend/src/components/eventForm.css
new file mode 100644
index 0000000..0175a81
--- /dev/null
+++ b/frontend/src/components/eventForm.css
@@ -0,0 +1,124 @@
+/* eventForm.css */
+.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;
+}
\ No newline at end of file
diff --git a/frontend/src/components/eventForm.jsx b/frontend/src/components/eventForm.jsx
new file mode 100644
index 0000000..d779894
--- /dev/null
+++ b/frontend/src/components/eventForm.jsx
@@ -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";
+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({
+ 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: {
+ 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
+ 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");
+ 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 (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..2c84af0
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,111 @@
+:root {
+ --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) {
+ :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);
+}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..9b10d14
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -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(
+
+
+
+
+
+);
diff --git a/frontend/src/schemas/eventPostSchema.js b/frontend/src/schemas/eventPostSchema.js
new file mode 100644
index 0000000..3a2914d
--- /dev/null
+++ b/frontend/src/schemas/eventPostSchema.js
@@ -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."),
+ url: z.string().url().nullable().optional(),
+ address: z.string().min(5, "Address is required"),
+
+ status: z.enum(["scheduled", "completed", "canceled"]).optional(),
+
+ // 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;
+ });
\ No newline at end of file
diff --git a/frontend/src/services/eventService.js b/frontend/src/services/eventService.js
new file mode 100644
index 0000000..b0b39ab
--- /dev/null
+++ b/frontend/src/services/eventService.js
@@ -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
+ 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();
+ }
+}
\ No newline at end of file
diff --git a/localist/settings.py b/localist/settings.py
index 1851b05..488b5ec 100644
--- a/localist/settings.py
+++ b/localist/settings.py
@@ -1,4 +1,8 @@
+import sys
+
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",