diff --git a/package-lock.json b/package-lock.json index 5614eae9..f1c15463 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,15 @@ "@react-google-maps/api": "^2.20.3", "@supabase/ssr": "^0.5.1", "@supabase/supabase-js": "^2.46.1", + "@tanstack/react-query": "^5.59.19", "deck.gl": "^9.0.34", "leaflet": "^1.9.4", "lucide-react": "^0.454.0", "maplibre-gl": "^4.7.1", "next": "15.0.2", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "sonner": "^1.7.0" }, "devDependencies": { "@types/node": "^22.8.7", @@ -1291,6 +1293,32 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.20.tgz", + "integrity": "sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.20.tgz", + "integrity": "sha512-Zly0egsK0tFdfSbh5/mapSa+Zfc3Et0Zkar7Wo5sQkFzWyB3p3uZWOHR2wrlAEEV2L953eLuDBtbgFvMYiLvUw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.59.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@turf/boolean-clockwise": { "version": "5.1.5", "license": "MIT", @@ -6021,6 +6049,16 @@ "version": "0.6.1", "license": "MIT" }, + "node_modules/sonner": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.0.tgz", + "integrity": "sha512-W6dH7m5MujEPyug3lpI2l3TC3Pp1+LTgK0Efg+IHDrBbtEjyCmCHHo6yfNBOsf1tFZ6zf+jceWwB38baC8yO9g==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/sortablejs": { "version": "1.15.3", "license": "MIT", @@ -6988,6 +7026,111 @@ "version": "0.1.5", "license": "MIT", "optional": true + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.2.tgz", + "integrity": "sha512-KUpBVxIbjzFiUZhiLIpJiBoelqzQtVZbdNNsehhUn36e2YzKHphnK8eTUW1s/4aPy5kH/UTid8IuVbaOpedhpw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.2.tgz", + "integrity": "sha512-9J7TPEcHNAZvwxXRzOtiUvwtTD+fmuY0l7RErf8Yyc7kMpE47MIQakl+3jecmkhOoIyi/Rp+ddq7j4wG6JDskQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.2.tgz", + "integrity": "sha512-BjH4ZSzJIoTTZRh6rG+a/Ry4SW0HlizcPorqNBixBWc3wtQtj4Sn9FnRZe22QqrPnzoaW0ctvSz4FaH4eGKMww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.2.tgz", + "integrity": "sha512-i3U2TcHgo26sIhcwX/Rshz6avM6nizrZPvrDVDY1bXcLH1ndjbO8zuC7RoHp0NSK7wjJMPYzm7NYL1ksSKFreA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.2.tgz", + "integrity": "sha512-AMfZfSVOIR8fa+TXlAooByEF4OB00wqnms1sJ1v+iu8ivwvtPvnkwdzzFMpsK5jA2S9oNeeQ04egIWVb4QWmtQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.2.tgz", + "integrity": "sha512-JkXysDT0/hEY47O+Hvs8PbZAeiCQVxKfGtr4GUpNAhlG2E0Mkjibuo8ryGD29Qb5a3IOnKYNoZlh/MyKd2Nbww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.2.tgz", + "integrity": "sha512-foaUL0NqJY/dX0Pi/UcZm5zsmSk5MtP/gxx3xOPyREkMFN+CTjctPfu3QaqrQHinaKdPnMWPJDKt4VjDfTBe/Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index 219112dc..51a26afc 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,15 @@ "@react-google-maps/api": "^2.20.3", "@supabase/ssr": "^0.5.1", "@supabase/supabase-js": "^2.46.1", + "@tanstack/react-query": "^5.59.19", "deck.gl": "^9.0.34", "leaflet": "^1.9.4", "lucide-react": "^0.454.0", "maplibre-gl": "^4.7.1", "next": "15.0.2", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "sonner": "^1.7.0" }, "devDependencies": { "@types/node": "^22.8.7", @@ -32,9 +34,9 @@ "eslint-config-prettier": "^9.1.0", "postcss": "^8", "prettier": "3.3.3", + "supabase": "^1.215.0", "tailwindcss": "^3.4.1", - "typescript": "^5.6.3", - "supabase": "^1.215.0" + "typescript": "^5.6.3" }, "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" } diff --git a/src/app/auth/page.js b/src/app/auth/page.js index 8d286b37..5397547f 100644 --- a/src/app/auth/page.js +++ b/src/app/auth/page.js @@ -18,7 +18,7 @@ export default function AuthPage() { }); return ( -
+
(window.location.href = '/')} />
); diff --git a/src/app/layout.js b/src/app/layout.tsx similarity index 50% rename from src/app/layout.js rename to src/app/layout.tsx index ff84eee2..3513c0bf 100644 --- a/src/app/layout.js +++ b/src/app/layout.tsx @@ -2,19 +2,18 @@ import './globals.css'; import 'leaflet/dist/leaflet.css'; import EmergencyLayout from '@/components/layout/EmergencyLayout'; import { EmergencyProvider } from '@/context/EmergencyProvider'; -import { TownsProvider } from '../context/TownProvider'; +import { TownsProvider } from '@/context/TownProvider'; import { createClient } from '@/lib/supabase/server'; -import { SessionProvider } from '../context/SessionProvider'; +import { SessionProvider } from '@/context/SessionProvider'; +import { townsService } from '@/lib/service'; +import { Toaster } from 'sonner'; +import { PropsWithChildren } from 'react'; +import { QueryClientProvider } from '@/context/QueryClientProvider'; export const metadata = { title: 'Ajuda Dana - Sistema de Coordinación', description: 'Sistema de coordinación para emergencias en la Comunidad Valenciana', }; -const getTowns = async () => { - const supabase = await createClient(); - const { data, error } = await supabase.from('towns').select('id, name'); - return data; -}; const getSession = async () => { const supabase = await createClient(); @@ -22,18 +21,21 @@ const getSession = async () => { return data; }; -export default async function RootLayout({ children }) { +export default async function RootLayout({ children }: PropsWithChildren) { const session = await getSession(); - const towns = await getTowns(); + const towns = await townsService.getTowns(); return ( + - - - {children} - - + + + + {children} + + + diff --git a/src/app/politica-privacidad/page.tsx b/src/app/politica-privacidad/page.tsx new file mode 100644 index 00000000..b7cf0c76 --- /dev/null +++ b/src/app/politica-privacidad/page.tsx @@ -0,0 +1,119 @@ +export default function politicaPrivacidad() { + return ( +
+

Politica de privacidad

+
+

1. Datos Personales que Recopilamos

+

+ Recopilamos datos personales para facilitar el contacto entre personas que ofrecen ayuda sin ánimo de lucro y + aquellas que la necesitan en las zonas afectadas por catástrofes naturales. Los datos recopilados incluyen: +

+
    +
  • + Datos obligatorios: correo electrónico para el registro y ubicación aproximada (barrio o área general). +
  • +
  • Datos opcionales: número de teléfono y descripción de las necesidades de ayuda.
  • +
+

+ Nota: No proporciones ningún dato que no esté indicado en el formulario, como nombre completo, DNI o cuentas + bancarias. +

+
+
+

2. Finalidad y Base Legal del Tratamiento de Datos

+
    +
  • Finalidad: Facilitar la conexión entre personas que ofrecen y requieren ayuda en las áreas afectadas.
  • +
  • + Base legal: El tratamiento de los datos se basa en el consentimiento expreso del usuario, otorgado al + registrarse y rellenar el formulario de solicitud de ayuda. +
  • +
+
+
+

3. Consentimiento para la Recopilación y Publicación de Datos

+

+ Al registrarse y rellenar el formulario de solicitud de ayuda, el usuario otorga su consentimiento para la + recopilación y, cuando lo autorice expresamente, la publicación de su ubicación aproximada en el mapa, número + de teléfono y necesidades. Este consentimiento es revocable en cualquier momento. +

+

+ Para revocar el consentimiento de publicación, envía un correo a{' '} + + info@ajudadana.es + + , indicando ELIMINACIÓN DE SOLICITUD DE AYUDA EN EL MAPA, junto con el correo de registro y la ubicación + aproximada. Responderemos en un plazo máximo de 30 días. +

+
+
+

4. Duración de Conservación de los Datos

+

+ Los datos serán utilizados durante la emergencia y hasta un máximo de 6 meses después de la normalización de + la situación, momento en el cual serán eliminados y destruidos de forma segura. +

+
+
+

5. Derechos de los Usuarios

+

De acuerdo con el RGPD, los usuarios tienen derecho a:

+
    +
  • Acceso: Solicitar una copia de sus datos personales.
  • +
  • Rectificación: Modificar información incorrecta.
  • +
  • Supresión: Solicitar la eliminación de sus datos en cualquier momento.
  • +
  • Oposición y Limitación del Tratamiento: Oponerse al tratamiento o limitar su uso.
  • +
  • Portabilidad: Solicitar que sus datos sean transferidos a otra entidad.
  • +
+

+ Para ejercer estos derechos, envía una solicitud a{' '} + + info@ajudadana.es + + , incluyendo el derecho específico que deseas ejercer. La solicitud se procesará en un plazo de 30 días. +

+
+
+

6. Seguridad de los Datos

+

+ Implementamos medidas técnicas y organizativas para proteger los datos personales. En caso de una violación de + seguridad que pueda afectar los derechos y libertades de los usuarios, notificaremos a la AEPD en un plazo de + 72 horas y, si procede, a los usuarios afectados. +

+
+
+

7. Transferencia de Datos a Terceros

+

+ Actualmente, Ayuda Dana no realiza transferencias de datos personales fuera del Espacio Económico Europeo + (EEE). Si esto cambia, se informará y solicitará el consentimiento explícito de los usuarios. +

+
+
+

8. Privacidad de los Menores

+

+ El sitio no está dirigido a menores de 18 años. Si un padre o tutor detecta que un menor ha proporcionado + datos personales sin el debido consentimiento, contacta con nosotros en{' '} + + info@ajudadana.es + {' '} + para su eliminación. +

+
+
+

9. Modificaciones de la Política de Privacidad

+

+ Ayuda Dana se reserva el derecho de modificar esta Política de Privacidad. En caso de cambios sustanciales, + notificaremos a los usuarios registrados y solicitaremos nuevamente su consentimiento si los cambios implican + un uso ampliado de sus datos. +

+
+
+

10. Contacto

+

+ Para preguntas sobre nuestra Política de Privacidad o el uso de tus datos, contáctanos en{' '} + + info@ajudadana.es + +

+
+

Última actualización: 5 de noviembre de 2024

+
+ ); +} diff --git a/src/app/punto-recogida/page.js b/src/app/punto-recogida/page.js index b993a420..e61daec3 100644 --- a/src/app/punto-recogida/page.js +++ b/src/app/punto-recogida/page.js @@ -1,13 +1,14 @@ 'use client'; -import { useState, useCallback } from 'react'; -import { Package } from 'lucide-react'; +import { useState, useCallback, useEffect } from 'react'; import { supabase } from '@/lib/supabase/client'; +import { MapPin, Phone, Package, House, Contact, Megaphone } from 'lucide-react'; +import AddressAutocomplete from '@/components/AddressAutocomplete'; import { isValidPhone } from '@/helpers/utils'; import { PhoneInput } from '@/components/PhoneInput'; -export default function PuntoRecogida() { - const [formData, setFormData] = useState({ +export default function PuntosRecogida() { + const initialFormData = { name: '', type: 'permanente', location: '', @@ -17,13 +18,14 @@ export default function PuntoRecogida() { accepted_items: [], urgent_needs: '', status: 'active', - }); + }; - const [status, setStatus] = useState({ - isSubmitting: false, - error: null, - success: false, - }); + const [formData, setFormData] = useState(initialFormData); + const [showForm, setShowForm] = useState(false); + const [loading, setLoading] = useState(false); + const [collectionPoints, setCollectionPoints] = useState([]); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); const tiposAyuda = ['Alimentos', 'Agua', 'Ropa', 'Mantas', 'Medicamentos', 'Productos de higiene']; @@ -31,18 +33,36 @@ export default function PuntoRecogida() { setFormData((formData) => ({ ...formData, contact_phone: phoneNumber })); }, []); - const handleSubmit = async (e) => { + useEffect(() => { + fetchCollectionPoints(); + }, []); + + async function fetchCollectionPoints() { + try { + let { data, error } = await supabase + .from('collection_points') + .select('*') + .order('created_at', { ascending: false }); + + if (error) throw error; + setCollectionPoints(data || []); + } catch (error) { + console.error('Error:', error); + setError('Error al cargar los puntos de recogida'); + } + } + + async function handleSubmit(e) { e.preventDefault(); - setStatus({ isSubmitting: true, error: null, success: false }); + setLoading(true); + setError(null); try { - // Validar campos requeridos - if (!formData.name || !formData.location || !formData.city || !formData.contact_name || !formData.contact_phone) { - throw new Error('Por favor, complete todos los campos obligatorios'); - } + const requiredFields = ['name', 'location', 'contact_phone']; + const missingFields = requiredFields.filter((field) => !formData[field]); - if (formData.accepted_items.length === 0) { - throw new Error('Seleccione al menos un tipo de ayuda'); + if (missingFields.length > 0) { + throw new Error('Por favor completa todos los campos obligatorios'); } if (!isValidPhone(formData.contact_phone)) { @@ -50,150 +70,261 @@ export default function PuntoRecogida() { return; } - // Insertar en Supabase directamente - const { error } = await supabase.from('collection_points').insert([formData]); - - if (error) throw error; - - // Limpiar formulario - setFormData({ - name: '', + const pointData = { + name: formData.name, type: 'permanente', - location: '', - city: '', - contact_name: '', - contact_phone: '', - accepted_items: [], - urgent_needs: '', + location: formData.location, + city: formData.city || null, + contact_name: formData.contact_name || null, + contact_phone: formData.contact_phone, + accepted_items: formData.accepted_items || [], + urgent_needs: formData.urgent_needs || null, status: 'active', - }); + }; + + const { error: insertError } = await supabase.from('collection_points').insert([pointData]); - setStatus({ isSubmitting: false, error: null, success: true }); - setTimeout(() => setStatus((prev) => ({ ...prev, success: false })), 5000); + if (insertError) throw insertError; + + await fetchCollectionPoints(); + setSuccess(true); + setShowForm(false); + setFormData(initialFormData); + + setTimeout(() => setSuccess(false), 3000); } catch (error) { - console.error('Error:', error); - setStatus({ - isSubmitting: false, - error: error.message || 'Error al registrar el punto de recogida', - success: false, - }); + console.error('Error al registrar punto de recogida:', error); + setError(error.message || 'Error al registrar el punto de recogida'); + } finally { + setLoading(false); } - }; + } return ( -
- {status.error && ( -
-

{status.error}

-
- )} - - {status.success && ( -
-

Punto de recogida registrado correctamente

-
- )} - -
-
+
+
+

-

Registrar Punto de Recogida

-

+ Puntos de recogida + + +
-
-
- - setFormData({ ...formData, name: e.target.value })} - className="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500" - /> -
+ {/* Lista de puntos de recogida */} +
+ {collectionPoints.length > 0 ? ( + collectionPoints.map((point) => ( +
+
+
+

{point.name}

+
+ + {point.location} +
+
+ + Referencia: {point.id} + + + {point.status === 'active' ? 'Activo' : 'Inactivo'} + +
-
- - setFormData({ ...formData, location: e.target.value })} - className="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500" - /> +
+ {point.city && ( +
+ + Ciudad: {point.city} +
+ )} + {point.contact_name && ( +
+ + Nombre de contacto: {point.contact_name} +
+ )} + {point.accepted_items && ( +
+ + + Necesita:{' '} + {Array.isArray(point.accepted_items) + ? point.accepted_items + .map((tipo) => { + return tipo; + }) + .join(', ') + : 'Ayuda general'} + +
+ )} + {point.contact_phone && ( +
+ + Teléfono: {point.contact_phone} +
+ )} + {point.urgent_needs && ( +
+ Necesidades urgentes: +

{point.urgent_needs}

+
+ )} +
+
+ )) + ) : ( +
+ +

No hay puntos de recogida registrados

+

+ Sé el primero en registrar un punto de recogida para ayudar con la logística de suministros. +

+
+ )} +
-
- - setFormData({ ...formData, city: e.target.value })} - className="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500" - /> -
+ {/* Modal2 de formulario */} + {showForm && ( +
+
+

Registrar Punto de Recogida

+ +
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full p-2 border rounded" + required + /> +
+
+ + { + setFormData((prev) => ({ + ...prev, + location: address.fullAddress, + city: address.details.city, + coordinates: address.coordinates + ? { + lat: address.coordinates.lat, + lng: address.coordinates.lon, + } + : null, + })); + }} + placeholder="Buscar dirección..." + /> +
+
+ +
+ {tiposAyuda.map((tipo) => ( + + ))} +
+
+
+
+ + setFormData({ ...formData, contact_name: e.target.value })} + className="w-full p-2 border rounded" + /> +
+ +
-
- - setFormData({ ...formData, contact_name: e.target.value })} - className="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500" - /> -
+
+ +