diff --git a/package-lock.json b/package-lock.json index 0a0ea68..bad17de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,10 @@ "name": "vite-react-starter", "version": "0.0.0", "dependencies": { + "leaflet": "^1.9.4", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.38.0", @@ -941,6 +943,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.43", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", @@ -2061,6 +2074,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2312,6 +2331,20 @@ "react": "^19.2.0" } }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -3165,6 +3198,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "requires": {} + }, "@rolldown/pluginutils": { "version": "1.0.0-beta.43", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", @@ -3920,6 +3959,11 @@ "json-buffer": "3.0.1" } }, + "leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4088,6 +4132,14 @@ "scheduler": "^0.27.0" } }, + "react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "requires": { + "@react-leaflet/core": "^3.0.0" + } + }, "react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/package.json b/package.json index 357d257..48a6403 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "leaflet": "^1.9.4", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0" }, "devDependencies": { "@eslint/js": "^9.38.0", diff --git a/src/App.css b/src/App.css index 4676756..3b2a3f2 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1,719 @@ -#root { - margin: 0 auto; - padding: 2rem; - text-align: center; - font-family: sans-serif; -} +/* =========================== + Global + design tokens + =========================== */ -.app { - display: flex; - flex-direction: column; -} + html { + scroll-behavior: smooth; + } + + :root { + --ng-bg-body-light: #e5f0ff; + --ng-bg-body-light-alt: #f5f7fb; + --ng-bg-surface-light: #ffffff; + + --ng-bg-body-dark: #020617; + --ng-bg-body-dark-alt: #020617; + --ng-bg-surface-dark: #020617; + + --ng-primary: #4f46e5; + --ng-primary-soft: #eef2ff; + --ng-accent: #22c55e; + --ng-danger: #ef4444; + + --ng-text-main-light: #0f172a; + --ng-text-soft-light: #4b5563; + --ng-text-main-dark: #e5e7eb; + --ng-text-soft-dark: #9ca3af; + + --ng-border-subtle-light: #e5e7eb; + --ng-border-strong-light: #cbd5f5; + --ng-border-subtle-dark: #1f2937; + --ng-border-strong-dark: #334155; + } + + *, + *::before, + *::after { + box-sizing: border-box; + } + + body { + margin: 0; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + } + + /* =========================== + App shell + =========================== */ + + .app-root { + min-height: 100vh; + background: radial-gradient(circle at 0% 0%, #dbeafe 0%, #f5f7fb 55%); + color: var(--ng-text-main-light); + } + + .app-root.theme-dark { + background: radial-gradient(circle at 0% 0%, #1d4ed8 0%, #020617 55%); + color: var(--ng-text-main-dark); + } + + /* keep content nicely centred on large screens */ + .header, + .app-content { + max-width: 1200px; + margin: 0 auto; + padding-left: 1.75rem; + padding-right: 1.75rem; + } + + .app-content { + padding-top: 1.5rem; + padding-bottom: 2.5rem; + } + + /* =========================== + Header + =========================== */ + + .header { + padding-top: 1.1rem; + padding-bottom: 1.1rem; + background: linear-gradient(to bottom, rgba(15, 23, 42, 0.98), rgba(15, 23, 42, 0.92)); + color: #f9fafb; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + border-bottom: 1px solid rgba(15, 23, 42, 0.75); + } + + .title { + margin: 0; + font-size: 1.8rem; + display: flex; + align-items: center; + } + + .subtitle { + margin: 0.25rem 0 0; + font-size: 0.95rem; + color: #e5e7eb; + } + + .header-badge { + font-size: 0.8rem; + padding: 0.35rem 0.75rem; + border-radius: 999px; + background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(148, 163, 184, 0.7); + } + + /* right side of header (points + theme toggle) */ + + .header-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.4rem; + } + + .points-display { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.85rem; + padding: 0.3rem 0.65rem; + border-radius: 999px; + background: rgba(37, 99, 235, 0.14); + color: #bfdbfe; + font-weight: 500; + } + + .points-user { + font-weight: 600; + } + + .theme-toggle { + border: none; + border-radius: 999px; + padding: 0.25rem 0.7rem; + font-size: 0.8rem; + background: rgba(15, 23, 42, 0.9); + color: #e5e7eb; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.3rem; + transition: background 0.15s ease, transform 0.1s ease; + } + + .theme-toggle:hover { + background: #111827; + transform: translateY(-1px); + } + + /* =========================== + Layout + =========================== */ + + .layout { + margin-top: 1.2rem; + display: grid; + grid-template-columns: minmax(260px, 360px) minmax(0, 1fr); + gap: 1rem; + align-items: stretch; + } + + @media (max-width: 900px) { + .layout { + grid-template-columns: 1fr; + } + } + + /* =========================== + Filter bar / quick actions + =========================== */ + + .filter-bar { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; + margin-top: 0.75rem; + } + + .filter-group { + display: flex; + flex-direction: column; + gap: 0.3rem; + font-size: 0.85rem; + } + + .filter-group label { + font-weight: 500; + } + + .filter-group select { + padding: 0.4rem 0.6rem; + border-radius: 0.5rem; + border: 1px solid #d1d5db; + background: #ffffff; + font-size: 0.9rem; + } + + /* “I have…” quick actions */ + + .quick-actions { + margin-bottom: 0.7rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.75rem; + } + + .quick-actions-label { + font-size: 0.85rem; + font-weight: 600; + color: #4b5563; + } + + .quick-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + .quick-action-pill { + border-radius: 999px; + border: 1px solid #e5e7eb; + background: #ffffff; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: background 0.12s ease, border-color 0.12s ease, + transform 0.08s ease, box-shadow 0.12s ease; + box-shadow: 0 6px 16px rgba(148, 163, 184, 0.25); + } + + .quick-action-pill:hover { + background: #eef2ff; + border-color: #4f46e5; + box-shadow: 0 10px 22px rgba(129, 140, 248, 0.45); + transform: translateY(-1px); + } + + .quick-action-emoji { + font-size: 1.2rem; + } + + /* =========================== + Impact summary + =========================== */ + + .summary-bar, + .impact-summary { + margin-top: 0.65rem; + padding: 0.55rem 0.75rem; + border-radius: 0.9rem; + background: rgba(255, 255, 255, 0.9); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.6rem; + font-size: 0.85rem; + color: #374151; + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08); + } + + .summary-pill { + padding: 0.18rem 0.6rem; + border-radius: 999px; + background: #f3f4f6; + font-size: 0.8rem; + } + + .summary-pill-primary { + background: #eef2ff; + color: #4338ca; + font-weight: 500; + } + + /* =========================== + Sidebar (charity list) + =========================== */ + + .sidebar { + background: #ffffff; + border-radius: 1rem; + padding: 0.9rem; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.12); + display: flex; + flex-direction: column; + } + + .sidebar-title { + margin: 0 0 0.7rem; + font-size: 0.95rem; + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.5rem; + } + + .sidebar-count { + font-size: 0.8rem; + color: #6b7280; + } + + .sidebar-list { + display: flex; + flex-direction: column; + gap: 0.6rem; + max-height: calc(100vh - 260px); + overflow-y: auto; + padding-right: 0.2rem; + } + + .sidebar-empty { + font-size: 0.9rem; + color: #6b7280; + } + + /* charity cards */ + + .charity-card { + border-radius: 0.9rem; + border: 1px solid #e5e7eb; + padding: 0.75rem 0.8rem; + background: #ffffff; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, + border-color 0.15s ease, background 0.15s ease; + } + + .charity-card:hover { + border-color: #c7d2fe; + box-shadow: 0 12px 26px rgba(15, 23, 42, 0.12); + transform: translateY(-2px); + } + + .charity-card-selected { + border-color: #4f46e5; + box-shadow: 0 14px 30px rgba(79, 70, 229, 0.4); + background: #f5f3ff; + } + + .charity-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + + .charity-name { + margin: 0; + font-size: 0.98rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.35rem; + } + + .charity-cause { + margin: 0.15rem 0; + font-size: 0.85rem; + color: #4b5563; + } + + .charity-description { + margin: 0.25rem 0 0.4rem; + font-size: 0.85rem; + color: #6b7280; + } + + .charity-meta { + display: flex; + justify-content: space-between; + gap: 0.5rem; + font-size: 0.78rem; + color: #6b7280; + margin-bottom: 0.4rem; + } + + .charity-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + + .charity-footer-actions { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + justify-content: flex-end; + } + + /* pills + buttons */ + + .pill { + padding: 0.15rem 0.5rem; + border-radius: 999px; + background: #eef2ff; + color: #4338ca; + font-size: 0.75rem; + font-weight: 500; + } + + .button { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + border: none; + padding: 0.35rem 0.9rem; + font-size: 0.8rem; + font-weight: 500; + background: #4f46e5; + color: #ffffff; + text-decoration: none; + cursor: pointer; + white-space: nowrap; + } + + .button-ghost { + background: #ffffff; + color: #4b5563; + border: 1px solid #d1d5db; + } + + .button-ghost:hover { + background: #f3f4f6; + } + + /* urgency tags */ + + .urgency-tag { + font-size: 0.7rem; + padding: 0.15rem 0.5rem; + border-radius: 999px; + font-weight: 500; + } + + .urgency-high { + background: #fee2e2; + color: #b91c1c; + } + + .urgency-medium { + background: #fef3c7; + color: #92400e; + } + + .urgency-low { + background: #dcfce7; + color: #166534; + } + + /* save button */ + + .save-button { + border: none; + background: transparent; + font-size: 0.75rem; + cursor: pointer; + color: #6b7280; + padding: 0.1rem 0.4rem; + border-radius: 999px; + transition: background 0.12s ease, color 0.12s ease; + } + + .save-button:hover { + background: #e5e7eb; + color: #374151; + } + + .save-button-active { + background: #fef3c7; + color: #92400e; + } + + /* =========================== + Map container + =========================== */ + + .map-container { + background: #ffffff; + border-radius: 1rem; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.12); + overflow: hidden; + min-height: 400px; + max-height: calc(100vh - 210px); + animation: fadeIn 0.4s ease; + } + + .map { + width: 100%; + height: 100%; + } + + /* ensure Leaflet has a background even before tiles load */ + .leaflet-container { + background: #e5e7eb; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* custom logo markers */ + + .charity-marker { + background: transparent; + border: none; + } + + .marker-inner { + width: 50px; + height: 50px; + border-radius: 999px; + background: #ffffff; + border: 3px solid #4f46e5; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.75rem; + box-shadow: 0 12px 26px rgba(15, 23, 42, 0.35); + transform: translateY(-10px); + transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease; + } + + .marker-inner-selected { + transform: translateY(-14px) scale(1.15); + border-color: #f97316; + box-shadow: 0 18px 34px rgba(251, 146, 60, 0.55); + } + + /* =========================== + Location status + =========================== */ + + .location-status { + margin-top: 0.7rem; + font-size: 0.85rem; + padding: 0.5rem 0.8rem; + border-radius: 0.7rem; + background: #eff6ff; + color: #1d4ed8; + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + + .location-status.error { + background: #fef2f2; + color: #b91c1c; + } + + .loader { + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #bfdbfe; + border-top-color: #4f46e5; + animation: spin 0.6s linear infinite; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + /* =========================== + Org banner + =========================== */ + + .org-banner { + margin-top: 1.4rem; + padding: 0.8rem 1rem; + border-radius: 0.9rem; + background: linear-gradient(to right, #f97316, #facc15); + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + color: #111827; + box-shadow: 0 16px 40px rgba(15, 23, 42, 0.1); + } + + .org-banner-text { + max-width: 70%; + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.85rem; + } + + .org-banner-label { + font-size: 0.9rem; + font-weight: 700; + } + + .org-banner-button { + border-radius: 999px; + background: #111827; + color: #f9fafb; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + text-decoration: none; + font-weight: 500; + white-space: nowrap; + } + + .org-banner-button:hover { + background: #020617; + } + + /* =========================== + Dark theme overrides + =========================== */ + + .theme-dark .header { + background: linear-gradient(to bottom, #020617, #020617); + border-bottom: 1px solid #111827; + } + + .theme-dark .subtitle { + color: #9ca3af; + } + + .theme-dark .app-content { + color: var(--ng-text-main-dark); + } + + .theme-dark .summary-bar, + .theme-dark .impact-summary { + background: rgba(15, 23, 42, 0.95); + color: #e5e7eb; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7); + } + + .theme-dark .summary-pill { + background: #1f2937; + } + + .theme-dark .summary-pill-primary { + background: #312e81; + color: #e5e7eb; + } + + .theme-dark .sidebar { + background: #020617; + border: 1px solid #1f2937; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7); + } + + .theme-dark .charity-card { + background: #020617; + border-color: #1f2937; + } + + .theme-dark .charity-card:hover { + border-color: #4f46e5; + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.9); + } + + .theme-dark .charity-card-selected { + background: #111827; + } + + .theme-dark .map-container { + background: #020617; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.75); + } + + /* give Leaflet a darker base so it blends with dark mode while tiles load */ + .theme-dark .leaflet-container { + background: #020617; + } + + .theme-dark .org-banner { + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7); + } + + .theme-dark .points-display { + background: rgba(37, 99, 235, 0.18); + color: #e5e7eb; + } + + .theme-dark .theme-toggle { + background: #1f2937; + } + + .theme-dark .theme-toggle:hover { + background: #374151; + } + + .theme-dark .location-status { + background: #111827; + color: #93c5fd; + } + + .theme-dark .location-status.error { + background: #111827; + color: #fecaca; + } + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index c15f767..5c627e1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,66 +1,182 @@ -import "./App.css"; -import Tier from "./components/Tier"; -import { useState } from "react"; +import React, { useMemo, useState } from "react"; +import Header from "./components/Header.jsx"; +import FilterBar from "./components/FilterBar.jsx"; +import Sidebar from "./components/Sidebar.jsx"; +import MapView from "./components/MapView.jsx"; +import Login from "./components/Login.jsx"; +import QuickActions from "./components/QuickActions.jsx"; +import ImpactSummary from "./components/ImpactSummary.jsx"; +import OrgBanner from "./components/OrgBanner.jsx"; +import { charities as baseCharities } from "./data/charities.js"; +import useUserLocation from "./hooks/useUserLocation.js"; +import { addDistanceToCharities } from "./utils/distance.js"; + function App() { - // const [counter, setCounter] = useState(0); - - // States for our controlled inputs - const [tier, setTier] = useState(""); - const [image, setImage] = useState(""); - const [name, setName] = useState(""); - - // States for our data - const [aTierItems, setATierItems] = useState([]); - const [fTierItems, setFTierItems] = useState([]); - - function addToTier() { - if (tier == "A") { - // Set the aTierItems list to... - setATierItems( - // A new list equalling whatever it is now, plus this new object added to the back of the list - aTierItems.concat({ - image: image, - name: name, - }) - ); - } else if (tier == "F") { - setFTierItems( - fTierItems.concat({ - image: image, - name: name, - }) - ); + + //STATE DECLARATIONS + const { location, loading: locationLoading, error: locationError } = + useUserLocation(); + + const [user, setUser] = useState(null); + const [theme, setTheme] = useState("light"); + const [points, setPoints] = useState(0); + const [savedIds, setSavedIds] = useState([]); + const [visitedIds, setVisitedIds] = useState([]); + + const [filters, setFilters] = useState({ + cause: "All", + accepts: "All", + sortBy: "distance", // "distance" | "urgency" + }); + + const [selectedCharityId, setSelectedCharityId] = useState(null); + + + // FUNCTIONS + + // global helper for impact points (called from CharityCard) + window.addNeighborGoodPoints = (amount) => { + setPoints((prev) => prev + amount); + }; + + // enrich charities with distance from user + const charitiesWithDistance = useMemo( + () => addDistanceToCharities(baseCharities, location), + [location] + ); + + // filters + sorting + const filteredCharities = useMemo(() => { + let list = [...charitiesWithDistance]; + + if (filters.cause !== "All") { + list = list.filter((c) => c.cause === filters.cause); + } + + if (filters.accepts !== "All") { + list = list.filter((c) => c.accepts.includes(filters.accepts)); + } + + if (filters.sortBy === "distance") { + list.sort((a, b) => { + if (a.distanceKm == null) return 1; + if (b.distanceKm == null) return -1; + return a.distanceKm - b.distanceKm; + }); + } else if (filters.sortBy === "urgency") { + const rank = { High: 0, Medium: 1, Low: 2 }; + list.sort((a, b) => rank[a.urgency] - rank[b.urgency]); + } + + return list; + }, [charitiesWithDistance, filters]); + + const selectedCharity = + filteredCharities.find((c) => c.id === selectedCharityId) || + filteredCharities[0] || + null; + + const handleToggleSave = (id) => { + setSavedIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + ); + }; + + // when selecting a charity (from sidebar or map): + // - mark as selected + // - record it as visited + // - scroll its card into view at the top + const handleSelectCharity = (id) => { + setSelectedCharityId(id); + + setVisitedIds((prev) => (prev.includes(id) ? prev : [...prev, id])); + + const el = document.getElementById(`charity-${id}`); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "start" }); } + }; + + // quick "I have..." filters: set donation type + urgency sort + const handleQuickFilter = (donationType) => { + setFilters((prev) => ({ + ...prev, + accepts: donationType, + sortBy: "urgency", + })); + }; + + // login/landing gate + if (!user) { + return ; } + const visitedCount = visitedIds.length; + const donationClicks = Math.floor(points / 10); // 10 pts per donate click + + + // UI return ( -
- {/* Default HTML-like input tags. Each tag is connected to and updates one state. */} - setTier(e.target.value)} - placeholder="Tier" +
+
+ setTheme((prev) => (prev === "light" ? "dark" : "light")) + } /> - setImage(e.target.value)} - placeholder="Image" - /> - setName(e.target.value)} - placeholder="Name" - /> - {/* A button that calls the addToTier function when clicked */} - - {/* This calls Tier(tier, list) in components/Tier.jsx */} - - +
+ {/* 1) "I have..." quick actions */} + + + {/* 2) Filter + sort bar */} + + + {/* 3) Impact summary + badges */} + + + {/* {locationLoading && ( +
+
+ + Detecting your location… you can still browse nearby charities. + +
+ )} */} + + {locationError && ( +
+ Could not access your location. Showing Vancouver defaults. +
+ )} + +
+ + + +
+ + {/* 4) Org signup banner at bottom */} + +
); } diff --git a/src/components/CharityCard.jsx b/src/components/CharityCard.jsx new file mode 100644 index 0000000..c587c59 --- /dev/null +++ b/src/components/CharityCard.jsx @@ -0,0 +1,122 @@ +import React from "react"; + +function UrgencyTag({ level }) { + const className = `urgency-tag urgency-${level.toLowerCase()}`; + return {level} need; +} + +function getCauseIcon(cause) { + switch (cause) { + case "Food Security": + return "🍎"; + case "Housing": + return "🏠"; + case "Health": + return "❤️"; + case "Indigenous Support": + return "🪶"; + case "Settlement": + return "🧭"; + case "Youth": + return "🎒"; + case "LGBTQ2S+": + return "🌈"; + case "Seniors": + return "👵"; + case "Environment": + return "🌿"; + case "Animals": + return "🐾"; + default: + return "🤝"; + } +} + +function CharityCard({ charity, isSelected, isSaved, onClick, onToggleSave }) { + const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( + `${charity.latitude},${charity.longitude}` + )}`; + + return ( +
+
+

+ {getCauseIcon(charity.cause)} + {charity.name} +

+
+ + +
+
+ +

{charity.cause}

+

{charity.description}

+ +
+ {charity.location} + {charity.distanceKm != null && ( + + {charity.distanceKm} km away + + )} +
+ +
+
+ {charity.accepts.map((type) => ( + + {type} + + ))} +
+
+ { + e.stopPropagation(); + }} + > + Open in Maps + + { + e.stopPropagation(); + if (typeof window.addNeighborGoodPoints === "function") { + window.addNeighborGoodPoints(10); // 10 points per click + } + }} + > + Donate or learn more + +
+
+ + {charity.impact && ( +

{charity.impact}

+ )} +
+ ); +} + +export default CharityCard; diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx new file mode 100644 index 0000000..1b6a219 --- /dev/null +++ b/src/components/FilterBar.jsx @@ -0,0 +1,59 @@ +import React from "react"; +import { causes, donationTypes } from "../data/charities.js"; + +function FilterBar({ filters, setFilters }) { + const handleChange = (field) => (event) => { + setFilters((prev) => ({ + ...prev, + [field]: event.target.value, + })); + }; + + return ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ ); +} + +export default FilterBar; diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..507d90d --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,57 @@ +import React from "react"; + +function NeighborGoodLogo() { + return ( + + ); +} + +function Header({ user, points, theme, onToggleTheme }) { + return ( +
+
+

+ + NeighborGood +

+

+ Discover nearby charities and high-impact causes across Metro + Vancouver, based on where you are. +

+
+
+ {user && ( +
+ Hi, {user.name} + {points} impact points +
+ )} + + + +
+ Built for HackCamp 2025 · Best Hack for Social Good +
+
+
+ ); +} + +export default Header; diff --git a/src/components/ImpactSummary.jsx b/src/components/ImpactSummary.jsx new file mode 100644 index 0000000..9bcc4fd --- /dev/null +++ b/src/components/ImpactSummary.jsx @@ -0,0 +1,40 @@ +import React from "react"; + +function ImpactSummary({ visitedCount, donationClicks, points }) { + const badges = []; + + if (visitedCount >= 3) { + badges.push({ icon: "🧭", label: "Neighbourhood Explorer" }); + } + if (donationClicks >= 2) { + badges.push({ icon: "🍞", label: "Food Hero" }); + } + if (points >= 50) { + badges.push({ icon: "🏙", label: "Community Builder" }); + } + + return ( +
+
+ Your impact this session + + Visited {visitedCount} charities · + Clicked through to {donationClicks} donation pages · + {points} impact points + +
+ {badges.length > 0 && ( +
+ {badges.map((badge) => ( + + {badge.icon} + {badge.label} + + ))} +
+ )} +
+ ); +} + +export default ImpactSummary; diff --git a/src/components/Item.css b/src/components/Item.css deleted file mode 100644 index 66436da..0000000 --- a/src/components/Item.css +++ /dev/null @@ -1,8 +0,0 @@ -.item { - width: 150px; - background-color: lightgray; -} - -.logo { - width: 80px; -} diff --git a/src/components/Item.jsx b/src/components/Item.jsx deleted file mode 100644 index f091eac..0000000 --- a/src/components/Item.jsx +++ /dev/null @@ -1,18 +0,0 @@ -// From the folder this file is in (./) look for an Item.css file. -import "./Item.css"; - -// We define by writing a function called Item -function Item(props) { - // props = { image: "...", name: "..." } because we did - // Components can only return one thing. That's why we wrapped everything in a div. - return ( -
- -

{props.name}

-
- ); -} - -// "export": make this file import-able by other files -// "default": Item is the most important export of this file -export default Item; diff --git a/src/components/Leaderboard.jsx b/src/components/Leaderboard.jsx new file mode 100644 index 0000000..8905997 --- /dev/null +++ b/src/components/Leaderboard.jsx @@ -0,0 +1,68 @@ +import React from "react"; +import "/src/App.css" + +/** + * Leaderboard props: + * - data: Array<{name: string, points: number}> + * - currentUser: string (name of the signed-in user) + * - currentUserPoints: number (their points; optional if they're already in data) + */ +export default function Leaderboard({ data = [], currentUser, currentUserPoints }) { + // create a shallow copy so we don't mutate props + const merged = [...data]; + + // try to find user (case-insensitive) + const userIndex = merged.findIndex( + (d) => d.name.toLowerCase() === (currentUser || "").toLowerCase() + ); + + if (currentUser) { + if (userIndex === -1) { + // if user isn't in the data, add them using provided points or 0 + merged.push({ name: currentUser, points: currentUserPoints ?? 0 }); + } else if (typeof currentUserPoints === "number") { + // if present and points explicitly provided, update them + merged[userIndex].points = currentUserPoints; + } + } + + // sort descending by points + merged.sort((a, b) => b.points - a.points); + + // determine the index of the current user after sorting + const sortedUserIndex = currentUser + ? merged.findIndex((d) => d.name.toLowerCase() === currentUser.toLowerCase()) + : -1; + + const medalForPosition = (pos) => { + if (pos === 0) return "🥇"; + if (pos === 1) return "🥈"; + if (pos === 2) return "🥉"; + return null; + }; + + return ( +
+

Top Users

+
    + {merged.map((item, i) => { + const isCurrent = i === sortedUserIndex; + const medal = medalForPosition(i); + return ( +
  • + + {medal && } + {item.name} + + {item.points} +
  • + ); + })} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/Login.jsx b/src/components/Login.jsx new file mode 100644 index 0000000..8f38f7e --- /dev/null +++ b/src/components/Login.jsx @@ -0,0 +1,315 @@ +// src/components/Login.jsx +import React, { useEffect, useState } from "react"; + + +//login initial prompt +function Login({ onLogin }) { + const [role, setRole] = useState(null); // "donor" | "charity" | null + const [name, setName] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + const trimmed = name.trim(); + if (!trimmed) return; + console.log("Submitting login with:", { name: trimmed, role }); + onLogin({ name: trimmed, role }); + }; + + // optional: scroll-reveal for sections + useEffect(() => { + const sections = document.querySelectorAll(".reveal-section"); + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("visible"); + } + }); + }, + { threshold: 0.2 } + ); + + sections.forEach((sec) => observer.observe(sec)); + return () => observer.disconnect(); + }, []); + + + // RETURN + return ( + + // landing page stuff +
+ + + + + {/* Top nav */} +
+
+
NG
+
NeighborGood
+
+ + +
+ + {/*paragraph blocks */} + +
+ {/* Hero section */} +
+ {/* Left: story + value prop */} +
+
Vancouver prototype · HackCamp 2025
+

+ Turn spare food and time + into real support on your block. +

+

+ NeighborGood is a concept map for Metro Vancouver that matches + neighbours, students, and local organizations in real time – so + an extra meal, jacket, or hour between classes actually reaches + someone who needs it. +

+ +
    +
  • See nearby food banks, shelters, and grassroots orgs on a map.
  • +
  • Filter by what you have: meals, clothing, time, or funds.
  • +
  • Walk in with confidence knowing what each place accepts.
  • +
+ +
+ Vancouver-first + Social good + Students & neighbours +
+
+ + {/* Right: centered "Start NeighborGood" panel */} +
+
+
+

Start NeighborGood

+

+ Enter your name to explore the interactive map prototype and + discover organizations near you. +

+ + {/*initial interface*/} + {role === null && +
+

Are you a donor or a charity?

+ +
+ + + +
+
+} +{role === "donor" && ( +
+

Donor Login

+ +
+ setName(e.target.value)} + /> + + + + +
+
+ )} + + {/* 3. CHARITY LOGIN */} + {role === "charity" && ( +
+

Charity Login

+ +
+ setName(e.target.value)} + /> + + + + +
+
+ )} + +

+ No password needed for this prototype. A full version would + support separate neighbour and organization accounts. +

+
+
+
+
+ + {/* HOW IT WORKS */} +
+

How NeighborGood could work

+ +
+
+
+
+
+

Tell the app what you have

+

+ “I have 3 extra meals”, “I have a bag of clothes”, or “I can + volunteer for 1 hour.” Quick presets make it easy to act on + impulse instead of putting it off. +

+
+ +
+
+
+
+

See the closest, most relevant orgs

+

+ The map shows food banks, shelters, and student-led projects + that match what you can give, sorted by distance and urgency. +

+
+ +
+
+
+
+

Track small streaks of impact

+

+ Each visit earns NeighborGood points: not for clout, but to + nudge consistent micro-actions over time and highlight + under-supported organizations. +

+
+
+
+ + {/* For neighbours & students */} +
+

For neighbours & students

+ +
+
+
+
+
+
+
+ +
+

+ Maybe you cooked too much dinner, have an extra bag of clothing, + or can give an hour between classes. NeighborGood turns those + small, everyday moments into support for local organizations + that are usually hard to find. +

+

+ Instead of doomscrolling or feeling guilty, you can open the + map, see who is closest and most in need, and walk there with + clear, up-to-date info about what they accept. +

+
+
+
+ + {/* For organizations */} +
+

For organizations

+ +
+
+
+
+
+
+
+
+ +
+

+ Smaller food banks, grassroots mutual aid groups, and + student-led projects often rely on word-of-mouth. NeighborGood + gives you a dedicated place on the map so nearby donors can + discover you and bring the right kind of help. +

+

+ Through the “Register your org” concept in the app, we imagine + onboarding local partners and building a shared, community-owned + directory for Vancouver. +

+
+
+
+
+
+ ); +} + +export default Login; diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx new file mode 100644 index 0000000..ad75578 --- /dev/null +++ b/src/components/MapView.jsx @@ -0,0 +1,179 @@ +import React, { useMemo, useEffect } from "react"; +import { + MapContainer, + TileLayer, + Marker, + Popup, + useMap, +} from "react-leaflet"; +import L from "leaflet"; + +import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png"; +import markerIcon from "leaflet/dist/images/marker-icon.png"; +import markerShadow from "leaflet/dist/images/marker-shadow.png"; + +L.Icon.Default.mergeOptions({ + iconRetinaUrl: markerIcon2x, + iconUrl: markerIcon, + shadowUrl: markerShadow, +}); + +const defaultCenter = { + lat: 49.2665, + lng: -123.2499, // UBC-ish fallback +}; + +// Normal Leaflet icon for the user location +const userIcon = new L.Icon({ + iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png", + iconRetinaUrl: + "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png", + shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41], +}); + +function getCauseEmoji(cause) { + switch (cause) { + case "Food Security": + return "🍎"; + case "Housing": + return "🏠"; + case "Health": + return "❤️"; + case "Indigenous Support": + return "🪶"; + case "Settlement": + return "🧭"; + case "Youth": + return "🎒"; + case "LGBTQ2S+": + return "🌈"; + case "Seniors": + return "👵"; + case "Environment": + return "🌿"; + case "Animals": + return "🐾"; + default: + return "🤝"; + } +} + +// Big logo-style map marker +function createMarkerIcon(cause, isSelected) { + const emoji = getCauseEmoji(cause); + const extra = isSelected ? " marker-inner-selected" : ""; + + return L.divIcon({ + html: `
${emoji}
`, + className: "charity-marker", + iconSize: [60, 70], // outer clickable area + iconAnchor: [30, 70], // center bottom + popupAnchor: [0, -60], // popup above the marker + }); +} + +function MapAutoFit({ charities, userLocation }) { + const map = useMap(); + + const bounds = useMemo(() => { + const points = []; + + if (userLocation) { + points.push([userLocation.lat, userLocation.lng]); + } + + charities.forEach((c) => { + points.push([c.latitude, c.longitude]); + }); + + if (points.length === 0) return null; + + return L.latLngBounds(points); + }, [charities, userLocation]); + + useEffect(() => { + if (!bounds) return; + + map.fitBounds(bounds, { + padding: [40, 40], + maxZoom: 15, + }); + }, [bounds, map]); + + return null; +} + +function MapView({ charities, userLocation, selectedCharity, onSelectCharity }) { + const center = userLocation || defaultCenter; + + return ( +
+ + + + + + {userLocation && ( + + You are here. + + )} + + {charities.map((charity) => { + const isSelected = + selectedCharity && selectedCharity.id === charity.id; + + const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( + `${charity.latitude},${charity.longitude}` + )}`; + + return ( + onSelectCharity(charity.id), + }} + > + + {charity.name} +
+ {charity.cause} +
+ {charity.location} + {charity.distanceKm != null && ( + <> +
+ {charity.distanceKm} km away + + )} +
+ + Open in Maps + +
+
+ ); + })} +
+
+ ); +} + +export default MapView; diff --git a/src/components/OrgBanner.jsx b/src/components/OrgBanner.jsx new file mode 100644 index 0000000..feae9a4 --- /dev/null +++ b/src/components/OrgBanner.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +function OrgBanner() { + const formUrl = + "https://docs.google.com/forms/d/your-google-form-id-here/viewform"; + + return ( +
+
+ Are you a community organization? + + Food banks, shelters, mutual aid groups, student clubs—get listed + on NeighborGood so local donors can actually find you. + +
+ + Register your org now + +
+ ); +} + +export default OrgBanner; diff --git a/src/components/QuickActions.jsx b/src/components/QuickActions.jsx new file mode 100644 index 0000000..2efa07b --- /dev/null +++ b/src/components/QuickActions.jsx @@ -0,0 +1,45 @@ +import React from "react"; + +function QuickActions({ onQuickFilter }) { + return ( +
+ I have… +
+ + + + +
+
+ ); +} + +export default QuickActions; diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx new file mode 100644 index 0000000..51d5ff9 --- /dev/null +++ b/src/components/Sidebar.jsx @@ -0,0 +1,42 @@ +import React from "react"; +import CharityCard from "./CharityCard.jsx"; + +function Sidebar({ + charities, + selectedCharityId, + onSelectCharity, + savedIds, + onToggleSave, +}) { + return ( + + ); +} + +export default Sidebar; diff --git a/src/components/SummaryBar.jsx b/src/components/SummaryBar.jsx new file mode 100644 index 0000000..75ea9c1 --- /dev/null +++ b/src/components/SummaryBar.jsx @@ -0,0 +1,41 @@ +import React from "react"; + +function SummaryBar({ charities, points, savedCount }) { + if (!charities || charities.length === 0) return null; + + const total = charities.length; + const highUrgency = charities.filter((c) => c.urgency === "High").length; + const nearby = charities.filter( + (c) => c.distanceKm != null && c.distanceKm <= 3 + ).length; + + return ( +
+ + Showing {total} charities + {nearby > 0 && ( + <> + {" "} + · {nearby} within 3 km + + )} + {highUrgency > 0 && ( + <> + {" "} + · {highUrgency} high urgency + + )} + + + {savedCount > 0 && ( + Saved: {savedCount} + )} + + Impact points: {points} + + +
+ ); +} + +export default SummaryBar; diff --git a/src/components/Tier.jsx b/src/components/Tier.jsx deleted file mode 100644 index e3bccdd..0000000 --- a/src/components/Tier.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import Item from "./Item"; - -function Tier(props) { - return ( -
-

Tier {props.tier}

- - {/* Turns a list of the JavaScript objects to a list of rendered components */} - {/* Go through the list and for each object, use the data to make an */} - {props.list.map((item) => ( - // This calls Item(name, image) in components/Item.jsx - - ))} -
- ); -} - -export default Tier; diff --git a/src/components/orgaccount.jsx b/src/components/orgaccount.jsx new file mode 100644 index 0000000..c5d31a4 --- /dev/null +++ b/src/components/orgaccount.jsx @@ -0,0 +1,4 @@ +import React from "react"; + +function myfunc(){} +export default myfunc; \ No newline at end of file diff --git a/src/data/charities.js b/src/data/charities.js new file mode 100644 index 0000000..64290c2 --- /dev/null +++ b/src/data/charities.js @@ -0,0 +1,368 @@ +export const charities = [ + // FOOD SECURITY + { + id: "greater-van-food-bank", + name: "Greater Vancouver Food Bank", + cause: "Food Security", + description: + "Provides food support to individuals and families across Vancouver, Burnaby, New Westminster, and the North Shore.", + latitude: 49.2775, + longitude: -123.0894, + location: "295 Terminal Ave, Vancouver", + accepts: ["Food", "Money", "Time"], + urgency: "High", + website: "https://foodbank.bc.ca/", + impact: "Every dollar provides roughly two meals for a community member.", + }, + { + id: "ams-food-bank", + name: "AMS Food Bank (UBC)", + cause: "Food Security", + description: + "Offers emergency groceries and essentials to UBC students facing food insecurity.", + latitude: 49.2665, + longitude: -123.2499, + location: "UBC Nest, Vancouver", + accepts: ["Food", "Money", "Items"], + urgency: "High", + website: + "https://www.ams.ubc.ca/support-services/student-services/food-bank/", + impact: "Supports thousands of students every year with emergency groceries.", + }, + { + id: "community-fridge-mt-pleasant", + name: "Vancouver Community Fridge (Mount Pleasant)", + cause: "Food Security", + description: + "A community-run fridge offering free food to anyone who needs it.", + latitude: 49.259, + longitude: -123.101, + location: "Mount Pleasant, Vancouver", + accepts: ["Food", "Items"], + urgency: "Medium", + website: "https://communityfridgeproject.com/", + impact: "Makes fresh food accessible to all, 24 hours a day.", + }, + { + id: "kitsilano-nh-food", + name: "Kitsilano Neighbourhood House Food Program", + cause: "Food Security", + description: + "Provides community meals, food hampers, and nutrition programs.", + latitude: 49.2646, + longitude: -123.1666, + location: "2305 W 7th Ave, Vancouver", + accepts: ["Food", "Money", "Time"], + urgency: "Medium", + website: "https://www.kitshouse.org/", + impact: "Supports local families, seniors, and newcomers.", + }, + + // HOMELESSNESS & SURVIVAL SUPPORT + { + id: "ugm", + name: "Union Gospel Mission (UGM)", + cause: "Housing", + description: + "Provides meals, shelter, addiction recovery, and support services in the Downtown Eastside.", + latitude: 49.2825, + longitude: -123.0951, + location: "601 E Hastings St, Vancouver", + accepts: ["Food", "Clothing", "Hygiene", "Money", "Time"], + urgency: "High", + website: "https://ugm.ca/", + impact: "Serves thousands of meals per week to vulnerable community members.", + }, + { + id: "dewc", + name: "Downtown Eastside Women’s Centre", + cause: "Housing", + description: + "Provides safety, meals, clothing, and essential support for women in the DTES.", + latitude: 49.2796, + longitude: -123.0979, + location: "302 Columbia St, Vancouver", + accepts: ["Clothing", "Hygiene", "Food", "Money"], + urgency: "High", + website: "https://dewc.ca/", + impact: "Low-barrier safe space for women in the Downtown Eastside.", + }, + { + id: "covenant-house", + name: "Covenant House Vancouver", + cause: "Housing", + description: + "Provides housing, support, and outreach for youth experiencing homelessness.", + latitude: 49.2794, + longitude: -123.1272, + location: "1302 Seymour St, Vancouver", + accepts: ["Clothing", "Gift Cards", "Hygiene", "Money", "Time"], + urgency: "High", + website: "https://www.covenanthousebc.org/", + impact: "Supports over 1,500 at-risk youth annually.", + }, + { + id: "first-united", + name: "First United Church Community Ministry", + cause: "Housing", + description: + "Offers advocacy, shelter, legal services, and harm reduction support in the DTES.", + latitude: 49.2818, + longitude: -123.0987, + location: "320 E Hastings St, Vancouver", + accepts: ["Clothing", "Hygiene", "Food", "Money"], + urgency: "High", + website: "https://www.firstunited.ca/", + impact: "Delivers life-saving frontline services every day.", + }, + { + id: "raincity-housing", + name: "RainCity Housing", + cause: "Housing", + description: + "Provides specialized housing, shelter, and harm reduction services across Vancouver.", + latitude: 49.2807, + longitude: -123.0995, + location: "616 Powell St, Vancouver", + accepts: ["Clothing", "Hygiene", "Food", "Money"], + urgency: "High", + website: "https://www.raincityhousing.org/", + impact: "Supports thousands of people experiencing homelessness.", + }, + + // HEALTH & HOSPITAL SUPPORT + { + id: "michael-cuccione", + name: "Michael Cuccione Foundation", + cause: "Health", + description: "Funds childhood cancer research at BC Children’s Hospital.", + latitude: 49.2406, + longitude: -123.1162, + location: "BC Children’s Hospital, Vancouver", + accepts: ["Money"], + urgency: "Medium", + website: "https://www.childhoodcancerresearch.org/", + impact: "Supports major pediatric cancer breakthroughs.", + }, + { + id: "bc-childrens-hospital", + name: "BC Children’s Hospital Foundation", + cause: "Health", + description: + "Supports medical care, equipment, and research for children across BC.", + latitude: 49.2406, + longitude: -123.1162, + location: "4480 Oak St, Vancouver", + accepts: ["Money", "New Toys", "Books"], + urgency: "Medium", + website: "https://www.bcchf.ca/", + impact: "Improves care for over 85 thousand children annually.", + }, + { + id: "cmha", + name: "Canadian Mental Health Association (CMHA Vancouver)", + cause: "Health", + description: "Offers mental health programs, peer support, and advocacy.", + latitude: 49.2622, + longitude: -123.1023, + location: "110-2425 Quebec St, Vancouver", + accepts: ["Money", "Time"], + urgency: "Medium", + website: "https://vancouver-fraser.cmha.bc.ca/", + impact: "Provides mental health support to people of all ages.", + }, + + // INDIGENOUS-LED PROGRAMS + { + id: "vafcs", + name: "Vancouver Aboriginal Friendship Centre Society", + cause: "Indigenous Support", + description: + "Provides cultural, family, and wellness programs for Indigenous communities.", + latitude: 49.2823, + longitude: -123.0383, + location: "1607 E Hastings St, Vancouver", + accepts: ["Clothing", "Food", "Household Items", "Money"], + urgency: "High", + website: "https://www.vafcs.org/", + impact: "One of the largest Indigenous community hubs in Vancouver.", + }, + { + id: "unya", + name: "Urban Native Youth Association (UNYA)", + cause: "Indigenous Support", + description: + "Supports Indigenous youth through housing, mentorship, education, and wellness programs.", + latitude: 49.2627, + longitude: -123.0987, + location: "1618 E Hastings St, Vancouver", + accepts: ["Food", "Clothing", "Gift Cards", "Money"], + urgency: "High", + website: "https://unya.bc.ca/", + impact: "Over 20 culturally grounded programs for Indigenous youth.", + }, + { + id: "aboriginal-mother-centre", + name: "Aboriginal Mother Centre Society", + cause: "Indigenous Support", + description: + "Provides housing and essential support for at-risk Indigenous mothers and their children.", + latitude: 49.2607, + longitude: -123.0728, + location: "2019 Dundas St, Vancouver", + accepts: ["Baby Supplies", "Clothing", "Hygiene", "Money"], + urgency: "High", + website: "https://www.aboriginalmothercentre.ca/", + impact: "Helps stabilize families and prevent homelessness.", + }, + + // REFUGEE & NEWCOMER SUPPORT + { + id: "issbc", + name: "Immigrant Services Society of BC (ISSofBC)", + cause: "Settlement", + description: + "Supports refugees and newcomers with housing, education, and resettlement.", + latitude: 49.2604, + longitude: -123.1008, + location: "2610 Victoria Dr, Vancouver", + accepts: ["Clothing", "Household Items", "Food", "Money", "Time"], + urgency: "Medium", + website: "https://www.issbc.org/", + impact: "BC’s largest newcomer support network.", + }, + { + id: "mosaic", + name: "MOSAIC BC", + cause: "Settlement", + description: + "Provides support for immigrants, refugees, and families through education and employment programs.", + latitude: 49.2586, + longitude: -123.0412, + location: "5575 Boundary Rd, Vancouver", + accepts: ["Clothing", "Baby Supplies", "Gift Cards", "Money"], + urgency: "Medium", + website: "https://www.mosaicbc.org/", + impact: "Serves over 30,000 newcomers annually.", + }, + + // YOUTH & FAMILY + { + id: "big-brothers", + name: "Big Brothers of Greater Vancouver", + cause: "Youth", + description: + "Matches youth with mentors to build confidence and community belonging.", + latitude: 49.2636, + longitude: -123.1386, + location: "102-1193 Kingsway, Vancouver", + accepts: ["Clothing", "Money", "Time"], + urgency: "Medium", + website: "https://www.bigbrothersvancouver.com/", + impact: "Creates life-changing mentorship opportunities.", + }, + { + id: "ywca", + name: "YWCA Vancouver", + cause: "Youth", + description: + "Supports women, children, and families through housing and essential programs.", + latitude: 49.2683, + longitude: -123.1157, + location: "535 Hornby St, Vancouver", + accepts: ["Baby Supplies", "Clothing", "Household Items", "Money"], + urgency: "Medium", + website: "https://ywcavan.org/", + impact: "Provides critical services for families in need.", + }, + + // LGBTQ2S+ + { + id: "qmunity", + name: "QMUNITY", + cause: "LGBTQ2S+", + description: + "BC’s main queer, trans, and Two-Spirit resource centre offering programs and counselling.", + latitude: 49.2651, + longitude: -123.1374, + location: "610 Burrard St, Vancouver", + accepts: ["Hygiene", "Clothing", "Money"], + urgency: "Medium", + website: "https://qmunity.ca/", + impact: "Provides safe, affirming services for LGBTQ2S+ community members.", + }, + + // SENIORS + { + id: "seniors-services", + name: "Seniors Services Society of BC", + cause: "Seniors", + description: + "Offers housing, outreach, and food support for vulnerable seniors.", + latitude: 49.2044, + longitude: -122.9126, + location: "750 Carnarvon St, New Westminster", + accepts: ["Clothing", "Hygiene", "Food", "Money", "Time"], + urgency: "Medium", + website: "https://www.seniorsservicessociety.ca/", + impact: "Helps prevent homelessness among seniors.", + }, + + // ENVIRONMENT & ANIMALS + { + id: "ocean-wise", + name: "Ocean Wise Conservation", + cause: "Environment", + description: + "Protects oceans through research, education, and conservation programs.", + latitude: 49.3009, + longitude: -123.1306, + location: "845 Avison Way, Vancouver", + accepts: ["Money", "Time"], + urgency: "Low", + website: "https://ocean.org/", + impact: "Global conservation initiatives based in Vancouver.", + }, + { + id: "bc-spca", + name: "BC SPCA Vancouver", + cause: "Animals", + description: + "Provides rescue, shelter, and medical care for animals.", + latitude: 49.2643, + longitude: -123.1243, + location: "1205 E 7th Ave, Vancouver", + accepts: ["Pet Food", "Towels", "Money", "Time"], + urgency: "Medium", + website: "https://spca.bc.ca/", + impact: "One of BC’s primary animal protection organizations.", + }, +]; + +export const causes = [ + "All", + "Food Security", + "Housing", + "Health", + "Indigenous Support", + "Settlement", + "Youth", + "LGBTQ2S+", + "Seniors", + "Environment", + "Animals", +]; + +export const donationTypes = [ + "All", + "Food", + "Clothing", + "Hygiene", + "Baby Supplies", + "Household Items", + "Gift Cards", + "Pet Food", + "Items", + "Money", + "Time", +]; diff --git a/src/data/leaderboardData.json b/src/data/leaderboardData.json new file mode 100644 index 0000000..aec4f8b --- /dev/null +++ b/src/data/leaderboardData.json @@ -0,0 +1,12 @@ +[ + { "name": "Henry", "points": 1200 }, + { "name": "Ava", "points": 950 }, + { "name": "Liam", "points": 880 }, + { "name": "Sofia", "points": 760 }, + { "name": "Ethan", "points": 700 }, + { "name": "Jeremy", "points": 600 }, + { "name": "Mark", "points": 550 }, + { "name": "Daniel", "points": 380 }, + { "name": "Lenna", "points": 360 }, + { "name": "Palmer", "points": 200 } + ] \ No newline at end of file diff --git a/src/hooks/useUserLocation.js b/src/hooks/useUserLocation.js new file mode 100644 index 0000000..dbd8418 --- /dev/null +++ b/src/hooks/useUserLocation.js @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; + +function useUserLocation() { + const [location, setLocation] = useState(null); // { lat, lng } + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + if (!("geolocation" in navigator)) { + setLoading(false); + setError("Geolocation is not supported in this browser."); + return; + } + + navigator.geolocation.getCurrentPosition( + (pos) => { + setLocation({ + lat: pos.coords.latitude, + lng: pos.coords.longitude, + }); + setLoading(false); + }, + (err) => { + console.error("Geolocation error", err); + setError("Could not access your location."); + setLoading(false); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + } + ); + }, []); + + return { location, loading, error }; +} + +export default useUserLocation; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index e5fa72d..0000000 --- a/src/index.css +++ /dev/null @@ -1,12 +0,0 @@ -* { - margin: 0; - padding: 0; -} - -input { - width: 200px; -} - -button { - width: 200px; -} diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..1219456 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,10 +1,11 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.jsx"; +import "./styles.css"; +import "leaflet/dist/leaflet.css"; -createRoot(document.getElementById('root')).render( - +ReactDOM.createRoot(document.getElementById("root")).render( + - , -) + +); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..0c63c46 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,2931 @@ +/* ======================================= + GLOBAL / RESET + ======================================= */ + + html { + scroll-behavior: smooth; + } + + :root { + /* NeighborGood theme variables (light) */ + --ng-bg-body: #f3f4f6; + --ng-bg-surface: #ffffff; + --ng-bg-soft: #e5e7eb; + --ng-border-subtle: #e5e7eb; + --ng-border-strong: #cbd5f5; + --ng-text-main: #0f172a; + --ng-text-soft: #4b5563; + + /* brand colors */ + --ng-primary: #2563eb; + --ng-primary-soft: #dbeafe; + --ng-accent: #22c55e; + --ng-accent-soft: #dcfce7; + --ng-warm: #f97316; + + /* map */ + --ng-map-border: #cbd5f5; + + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + line-height: 1.5; + color: #111827; + background-color: #f3f4f6; + } + + .app-root.theme-dark { + --ng-bg-body: #020617; + --ng-bg-surface: #020617; + --ng-bg-soft: #020617; + --ng-border-subtle: #1f2937; + --ng-border-strong: #334155; + --ng-text-main: #e5e7eb; + --ng-text-soft: #9ca3af; + + --ng-primary: #4f46e5; + --ng-primary-soft: #312e81; + --ng-accent: #22c55e; + --ng-accent-soft: #14532d; + --ng-warm: #f97316; + } + + *, + *::before, + *::after { + box-sizing: border-box; + } + + body { + margin: 0; + } + + .app-root { + min-height: 100vh; + display: flex; + flex-direction: column; + } + + /* ======================================= + HEADER / POINTS / THEME TOGGLE + ======================================= */ + + .header { + padding: 1.5rem 2rem; + background: #0f172a; + color: #f9fafb; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + } + + .title { + margin: 0; + font-size: 1.8rem; + display: flex; + align-items: center; + } + + /* App header tagline */ +.subtitle { + margin: 0.3rem 0 0; + font-size: 1rem; + color: rgba(249, 250, 251, 0.96); /* almost pure white */ + letter-spacing: 0.01em; + max-width: 40rem; + text-shadow: 0 1px 3px rgba(15, 23, 42, 0.45); /* subtle glow for contrast */ +} + + .header-badge { + font-size: 0.85rem; + padding: 0.4rem 0.8rem; + border-radius: 999px; + background: #111827; + border: 1px solid #374151; + } + + .logo-wrapper { + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 0.6rem; + } + + .logo { + width: 30px; + height: 30px; + } + + .header-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.4rem; + } + + .points-display { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.85rem; + padding: 0.3rem 0.6rem; + border-radius: 999px; + background: #eef2ff; + color: #4338ca; + font-weight: 500; + } + + .points-user { + font-weight: 600; + } + + .points-value { + opacity: 0.9; + } + + .theme-toggle { + border: none; + border-radius: 999px; + padding: 0.25rem 0.7rem; + font-size: 0.8rem; + background: #e5e7eb; + color: #111827; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.3rem; + transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease; + } + + .theme-toggle:hover { + background: #d1d5db; + transform: translateY(-1px); + } + + /* ======================================= + MAIN APP LAYOUT (MAP + SIDEBAR VIEW) + ======================================= */ + + .app-content { + padding: 1rem 2rem 2rem; + } + + .layout { + margin-top: 1rem; + display: grid; + grid-template-columns: minmax(260px, 360px) minmax(0, 1fr); + gap: 1rem; + align-items: stretch; + } + + @media (max-width: 900px) { + .layout { + grid-template-columns: 1fr; + } + } + + /* Filters */ + + .filter-bar { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; + } + + .filter-group { + display: flex; + flex-direction: column; + gap: 0.3rem; + font-size: 0.85rem; + } + + .filter-group label { + font-weight: 500; + } + + .filter-group select { + padding: 0.4rem 0.6rem; + border-radius: 0.5rem; + border: 1px solid #d1d5db; + background: #ffffff; + font-size: 0.9rem; + } + + /* Location status */ + + .location-status { + margin-top: 0.6rem; + font-size: 0.85rem; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + background: #eff6ff; + color: #1d4ed8; + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + + .location-status.error { + background: #fef2f2; + color: #b91c1c; + } + + /* Sidebar list */ + + .sidebar { + background: #ffffff; + border-radius: 0.75rem; + padding: 1rem; + box-shadow: 0 10px 25px rgba(15, 23, 42, 0.04); + display: flex; + flex-direction: column; + } + + .sidebar-title { + margin: 0 0 0.75rem; + font-size: 1rem; + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.5rem; + } + + .sidebar-count { + font-size: 0.8rem; + color: #6b7280; + } + + .sidebar-list { + display: flex; + flex-direction: column; + gap: 0.6rem; + max-height: calc(100vh - 260px); + overflow-y: auto; + padding-right: 0.2rem; + } + + .sidebar-empty { + font-size: 0.9rem; + color: #6b7280; + } + + /* Charity card */ + + .charity-card { + border-radius: 0.75rem; + border: 1px solid #e5e7eb; + padding: 0.75rem 0.8rem; + background: #ffffff; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, + border-color 0.15s ease, background 0.15s ease; + } + + .charity-card:hover { + border-color: #c7d2fe; + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); + } + + .charity-card-selected { + border-color: #4f46e5; + box-shadow: 0 10px 22px rgba(79, 70, 229, 0.15); + background: #f5f3ff; + } + + .charity-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + + .charity-name { + margin: 0; + font-size: 0.98rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.35rem; + } + + .charity-icon { + font-size: 1rem; + } + + .charity-header-right { + display: flex; + align-items: center; + gap: 0.4rem; + } + + .charity-cause { + margin: 0.15rem 0; + font-size: 0.85rem; + color: #4b5563; + } + + .charity-description { + margin: 0.25rem 0 0.4rem; + font-size: 0.85rem; + color: #6b7280; + } + + .charity-meta { + display: flex; + justify-content: space-between; + gap: 0.5rem; + font-size: 0.78rem; + color: #6b7280; + margin-bottom: 0.4rem; + } + + .charity-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + } + + .charity-accepts { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + } + + .charity-impact { + margin: 0.4rem 0 0; + font-size: 0.78rem; + color: #4b5563; + } + + .charity-footer-actions { + display: flex; + flex-wrap: wrap; + gap: 0.3rem; + justify-content: flex-end; + } + + /* Pills & buttons */ + .small-button{ + justify-content: flex; + padding: 0.3rem 0.7rem; + text-align: center; + font-size: 0.9rem; + margin-bottom: 1rem; + background: #4f46e5; + color: #ffffff; + border-radius: 999px; + align-items: center; + } + + .pill { + padding: 0.15rem 0.5rem; + border-radius: 999px; + background: #eef2ff; + color: #4338ca; + font-size: 0.75rem; + font-weight: 500; + } + + .button { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + border: none; + padding: 0.35rem 0.8rem; + font-size: 0.8rem; + font-weight: 500; + background: #4f46e5; + color: #ffffff; + text-decoration: none; + cursor: pointer; + white-space: nowrap; + } + + .button-small { + padding: 0.3rem 0.7rem; + } + + .button-ghost { + background: #ffffff; + color: #4b5563; + border: 1px solid #d1d5db; + } + + .button-ghost:hover { + background: #f3f4f6; + } + + /* Urgency tags */ + + .urgency-tag { + font-size: 0.7rem; + padding: 0.15rem 0.5rem; + border-radius: 999px; + font-weight: 500; + } + + .urgency-high { + background: #fee2e2; + color: #b91c1c; + } + + .urgency-medium { + background: #fef3c7; + color: #92400e; + } + + .urgency-low { + background: #dcfce7; + color: #166534; + } + + /* Map container */ + + .map-container { + background: #ffffff; + border-radius: 0.75rem; + box-shadow: 0 10px 25px rgba(15, 23, 42, 0.04); + overflow: hidden; + min-height: 360px; + max-height: calc(100vh - 210px); + animation: fadeIn 0.4s ease; + } + + .map { + width: 100%; + height: 100%; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* Custom logo markers */ + + .charity-marker { + background: transparent; + border: none; + } + + .marker-inner { + width: 50px; + height: 50px; + border-radius: 999px; + background: #ffffff; + border: 3px solid #4f46e5; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.75rem; + box-shadow: 0 12px 26px rgba(15, 23, 42, 0.35); + transform: translateY(-10px); + transition: transform 0.12s ease, box-shadow 0.12s ease, + border-color 0.12s ease; + } + + .marker-inner-selected { + transform: translateY(-14px) scale(1.15); + border-color: #f97316; + box-shadow: 0 18px 34px rgba(251, 146, 60, 0.55); + } + + /* Quick "I have..." pills (in app view) */ + + .quick-actions { + margin-bottom: 0.75rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.75rem; + } + + .quick-actions-label { + font-size: 0.85rem; + font-weight: 600; + color: #4b5563; + } + + .quick-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + .quick-action-pill { + border-radius: 999px; + border: 1px solid #e5e7eb; + background: #ffffff; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: background 0.12s ease, border-color 0.12s ease, + transform 0.08s ease, box-shadow 0.12s ease; + box-shadow: 0 6px 16px rgba(148, 163, 184, 0.25); + } + + .quick-action-pill:hover { + background: #eef2ff; + border-color: #4f46e5; + box-shadow: 0 10px 22px rgba(129, 140, 248, 0.45); + transform: translateY(-1px); + } + + .quick-action-emoji { + font-size: 1.2rem; + } + + .quick-action-text { + white-space: nowrap; + } + + /* Impact summary bar */ + + .impact-summary { + margin-top: 0.3rem; + margin-bottom: 0.6rem; + padding: 0.55rem 0.75rem; + border-radius: 0.75rem; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + font-size: 0.85rem; + } + + .impact-main { + display: flex; + flex-direction: column; + gap: 0.15rem; + } + + .impact-main-title { + font-weight: 600; + color: #374151; + } + + .impact-main-stats { + color: #4b5563; + } + + .impact-badges { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + justify-content: flex-end; + } + + .impact-badge { + padding: 0.18rem 0.55rem; + border-radius: 999px; + background: #eef2ff; + color: #4338ca; + font-size: 0.8rem; + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-weight: 500; + } + + .impact-badge-icon { + font-size: 0.95rem; + } + + /* Save button in cards */ + + .save-button { + border: none; + background: transparent; + font-size: 0.75rem; + cursor: pointer; + color: #6b7280; + padding: 0.1rem 0.4rem; + border-radius: 999px; + transition: background 0.12s ease, color 0.12s ease; + } + + .save-button:hover { + background: #e5e7eb; + color: #374151; + } + + .save-button-active { + background: #fef3c7; + color: #92400e; + } + + /* Summary bar */ + + .summary-bar { + margin-top: 0.75rem; + padding: 0.5rem 0.75rem; + border-radius: 0.75rem; + background: #e5e7eb; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.85rem; + color: #374151; + flex-wrap: wrap; + gap: 0.5rem; + } + + .summary-right { + display: flex; + gap: 0.4rem; + align-items: center; + } + + .summary-pill { + padding: 0.15rem 0.55rem; + border-radius: 999px; + background: #f3f4f6; + font-size: 0.8rem; + } + + .summary-pill-primary { + background: #eef2ff; + color: #4338ca; + font-weight: 500; + } + + /* Loader */ + + .loader { + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #bfdbfe; + border-top-color: #4f46e5; + animation: spin 0.6s linear infinite; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + /* Org signup banner */ + + .org-banner { + margin-top: 0.9rem; + padding: 0.7rem 0.9rem; + border-radius: 0.9rem; + background: linear-gradient(to right, #f97316, #facc15); + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + color: #111827; + } + + .org-banner-text { + max-width: 70%; + display: flex; + flex-direction: column; + gap: 0.2rem; + } + + @media (max-width: 900px) { + .org-banner-text { + max-width: 100%; + } + } + + .org-banner-label { + font-size: 0.9rem; + font-weight: 700; + } + + .org-banner-body { + font-size: 0.85rem; + } + + .org-banner-button { + border-radius: 999px; + background: #111827; + color: #f9fafb; + padding: 0.45rem 0.9rem; + font-size: 0.85rem; + text-decoration: none; + font-weight: 500; + white-space: nowrap; + } + + .org-banner-button:hover { + background: #020617; + } + + /* ======================================= + DARK THEME OVERRIDES + ======================================= */ + + .theme-dark { + background-color: #020617; + color: #e5e7eb; + } + + .theme-dark .header { + background: #020617; + border-bottom: 1px solid #1f2937; + } + + .theme-dark .subtitle { + color: #9ca3af; + } + + .theme-dark .app-content { + background: #020617; + } + + .theme-dark .sidebar { + background: #020617; + border: 1px solid #1f2937; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35); + } + + .theme-dark .map-container { + background: #020617; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35); + } + + .theme-dark .charity-card { + background: #020617; + border-color: #1f2937; + } + + .theme-dark .charity-card:hover { + border-color: #4f46e5; + box-shadow: 0 14px 30px rgba(31, 41, 55, 0.7); + } + + .theme-dark .charity-card-selected { + background: #111827; + } + + .theme-dark .summary-bar { + background: #111827; + color: #e5e7eb; + } + + .theme-dark .summary-pill { + background: #1f2937; + } + + .theme-dark .summary-pill-primary { + background: #312e81; + color: #e5e7eb; + } + + .theme-dark .points-display { + background: #1f2937; + color: #e5e7eb; + } + + .theme-dark .theme-toggle { + background: #1f2937; + color: #e5e7eb; + } + + .theme-dark .theme-toggle:hover { + background: #374151; + } + + .theme-dark .location-status { + background: #111827; + color: #93c5fd; + } + + .theme-dark .location-status.error { + background: #111827; + color: #fecaca; + } + + /* ======================================= + LANDING / HERO PAGE (NEIGHBORGOOD) + ======================================= */ + + /* Background */ + + .landing-root { + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; + color: #f9fafb; + background: radial-gradient(circle at 0% 0%, #1d4ed8 0%, #020617 55%); + } + + /* Top nav */ + + .landing-nav { + position: sticky; + top: 0; + z-index: 20; + padding: 1rem 2.5rem; + display: flex; + align-items: center; + justify-content: space-between; + backdrop-filter: blur(14px); + background: linear-gradient( + to bottom, + rgba(15, 23, 42, 0.9), + rgba(15, 23, 42, 0.7), + transparent + ); + } + + .landing-logo { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .landing-logo-mark { + width: 28px; + height: 28px; + border-radius: 999px; + background: linear-gradient(135deg, #22c55e, #38bdf8); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: 700; + } + + .landing-logo-text { + font-size: 0.9rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #e5e7eb; + } + + .landing-nav-links { + display: flex; + gap: 1.6rem; + } + + .landing-nav-link { + border: none; + background: none; + font-size: 0.8rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #9ca3af; + cursor: pointer; + } + + .landing-nav-link:hover { + color: #f9fafb; + } + + /* Main hero layout */ + + .landing-main { + padding: 2.2rem 2.5rem 2.5rem; + } + + @media (max-width: 960px) { + .landing-main { + padding: 1.8rem 1.4rem 2.2rem; + } + } + + .landing-hero { + display: grid; + grid-template-columns: minmax(0, 3.2fr) minmax(0, 3fr); + gap: 2rem; + align-items: center; + } + + @media (max-width: 960px) { + .landing-hero { + grid-template-columns: minmax(0, 1fr); + } + } + + /* Hero left text */ + + .landing-hero-left { + max-width: 560px; + } + + .landing-pill { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.75rem; + border-radius: 999px; + border: 1px solid rgba(191, 219, 254, 0.9); + font-size: 0.75rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #e5e7eb; + margin-bottom: 0.75rem; + background: rgba(15, 23, 42, 0.55); + } + + .landing-title { + font-size: 2.7rem; + line-height: 1.05; + font-weight: 800; + letter-spacing: -0.04em; + } + + .landing-title span { + display: block; + background: linear-gradient(135deg, #60a5fa, #22c55e); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } + +/* Landing hero subtitle under NeighborGood logo/title */ +.landing-subtitle { + margin-top: 0.75rem; + font-size: 1.05rem; + line-height: 1.6; + max-width: 44rem; + color: rgba(249, 250, 251, 0.96); /* much higher contrast */ + letter-spacing: 0.005em; + text-shadow: 0 1px 4px rgba(15, 23, 42, 0.5); +} + + .landing-points { + margin-top: 0.75rem; + padding-left: 1.1rem; + font-size: 0.9rem; + color: #e5e7eb; + } + + .landing-tags { + margin-top: 0.9rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + .landing-tag { + font-size: 0.78rem; + padding: 0.3rem 0.7rem; + border-radius: 999px; + background: rgba(15, 23, 42, 0.7); + border: 1px solid rgba(148, 163, 184, 0.7); + } + + /* Hero right – centered "Start NeighborGood" panel */ + + .landing-hero-right { + display: flex; + align-items: center; + justify-content: center; + } + + .landing-start-wrapper { + width: 100%; + display: flex; + justify-content: center; + margin-top: 1.5rem; + } + + .landing-login-panel { + width: 100%; + max-width: 520px; + margin: 0 auto; + border-radius: 1.2rem; + background: rgba(15, 23, 42, 0.96); + border: 1px solid rgba(148, 163, 184, 0.7); + padding: 1.4rem 1.5rem; + box-shadow: 0 24px 45px rgba(15, 23, 42, 0.85); + } + + .landing-login-panel h2 { + text-align: center; + font-size: 1.3rem; + font-weight: 700; + margin-bottom: 0.6rem; + } + + .landing-login-panel p { + text-align: center; + font-size: 0.9rem; + margin-bottom: 1rem; + color: #e5e7eb; + } + + .landing-login-form { + display: flex; + flex-direction: column; + gap: 0.4rem; + } + + .landing-login-form label { + font-size: 0.78rem; + color: #cbd5f5; + } + + .landing-login-form input { + height: 48px; + border-radius: 0.8rem; + border: 1px solid #4b5563; + background: #020617; + padding: 0.5rem 0.7rem; + font-size: 1rem; + color: #f9fafb; + } + + .landing-login-form input:focus { + outline: none; + border-color: #60a5fa; + box-shadow: 0 0 0 1px #60a5fa; + } + + .landing-login-button { + margin-top: 0.4rem; + width: 100%; + height: 48px; + font-size: 1rem; + border-radius: 0.8rem; + font-weight: 600; + } + + .landing-login-note { + margin-top: 0.8rem; + text-align: center; + font-size: 0.78rem; + opacity: 0.75; + color: #9ca3af; + } + + /* Sections ("How it works", "For neighbours", "For organizations") */ + + .landing-section { + margin-top: 2.6rem; + } + + .landing-section-title { + font-size: 1rem; + font-weight: 600; + color: #e5e7eb; + margin-bottom: 0.9rem; + } + + /* three-column grid for How it works */ + + .landing-section-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; + } + + @media (max-width: 960px) { + .landing-section-grid { + grid-template-columns: minmax(0, 1fr); + } + } + + /* generic cards */ + + .landing-section-card { + border-radius: 1.1rem; + background: rgba(15, 23, 42, 0.96); + border: 1px solid rgba(148, 163, 184, 0.6); + padding: 0.9rem 1rem; + box-shadow: 0 18px 35px rgba(15, 23, 42, 0.8); + } + + .landing-section-card h3 { + font-size: 0.9rem; + font-weight: 600; + margin-bottom: 0.3rem; + } + + .landing-section-card p { + font-size: 0.83rem; + color: #d1d5db; + } + + /* mini line "icons" at top of each card */ + + .section-icon-container { + margin-bottom: 0.5rem; + } + + .section-icon { + width: 40px; + height: 40px; + border-radius: 999px; + border: 2px solid rgba(148, 163, 184, 0.9); + position: relative; + } + + /* simple variants */ + + .icon-have::before { + content: ""; + position: absolute; + left: 8px; + right: 8px; + top: 50%; + height: 2px; + background: linear-gradient(to right, #22c55e, #38bdf8); + transform: translateY(-50%); + } + + .icon-map::before { + content: ""; + position: absolute; + inset: 10px; + border-radius: 10px; + border: 1px solid #60a5fa; + } + + .icon-map::after { + content: ""; + position: absolute; + top: 10px; + left: 16px; + width: 8px; + height: 8px; + border-radius: 999px; + border: 2px solid #22c55e; + } + + .icon-badge::before { + content: ""; + position: absolute; + inset: 8px; + border-radius: 999px; + border: 1px dashed #facc15; + } + + .icon-badge::after { + content: ""; + position: absolute; + bottom: 6px; + left: 50%; + width: 10px; + height: 10px; + transform: translateX(-50%) rotate(45deg); + border-radius: 2px; + background: #22c55e; + } + + /* wide row layout for neighbour/org sections */ + + .landing-section-wide { + border-radius: 1.1rem; + background: rgba(15, 23, 42, 0.96); + border: 1px solid rgba(148, 163, 184, 0.6); + padding: 0.9rem 1rem; + box-shadow: 0 18px 35px rgba(15, 23, 42, 0.8); + display: grid; + grid-template-columns: 200px minmax(0, 1fr); + gap: 0.9rem; + } + + @media (max-width: 960px) { + .landing-section-wide { + grid-template-columns: minmax(0, 1fr); + } + } + + .landing-section-text p + p { + margin-top: 0.35rem; + } + + /* line illustrations for neighbour/org sections */ + + .line-illustration { + position: relative; + border-radius: 0.9rem; + background: radial-gradient(circle at 0% 0%, #1d4ed8 0%, #020617 70%); + border: 1px solid rgba(148, 163, 184, 0.6); + overflow: hidden; + } + + /* neighbours: two people + bag outline */ + + .line-person { + position: absolute; + bottom: 20px; + left: 26px; + width: 26px; + height: 54px; + border-radius: 14px; + border: 2px solid rgba(148, 163, 184, 0.8); + } + + .line-person::before { + content: ""; + position: absolute; + top: -18px; + left: 4px; + width: 18px; + height: 18px; + border-radius: 999px; + border: 2px solid rgba(148, 163, 184, 0.9); + } + + .line-person-2 { + left: 64px; + height: 50px; + } + + .line-bag { + position: absolute; + bottom: 16px; + right: 26px; + width: 34px; + height: 32px; + border-radius: 8px; + border: 2px solid rgba(56, 189, 248, 0.9); + } + + .line-bag::before { + content: ""; + position: absolute; + top: -8px; + left: 10px; + width: 14px; + height: 8px; + border-radius: 6px 6px 0 0; + border: 2px solid rgba(56, 189, 248, 0.9); + border-bottom: none; + } + + /* orgs: simple building + boxes */ + + .line-building { + position: absolute; + bottom: 18px; + left: 22px; + width: 52px; + height: 48px; + border-radius: 6px; + border: 2px solid rgba(148, 163, 184, 0.9); + } + + .line-building::before, + .line-building::after { + content: ""; + position: absolute; + width: 10px; + height: 10px; + border-radius: 3px; + border: 1px solid rgba(148, 163, 184, 0.9); + } + + .line-building::before { + top: 10px; + left: 8px; + } + + .line-building::after { + top: 10px; + right: 8px; + } + + .line-box { + position: absolute; + bottom: 20px; + right: 26px; + width: 26px; + height: 20px; + border-radius: 4px; + border: 2px solid rgba(96, 165, 250, 0.9); + } + + .line-box-2 { + right: 58px; + bottom: 26px; + border-color: rgba(22, 163, 74, 0.9); + } + + /* Scroll reveal */ + + .reveal-section { + opacity: 0; + transform: translateY(26px); + transition: opacity 0.6s ease, transform 0.6s ease; + } + + .reveal-section.visible { + opacity: 1; + transform: translateY(0); + } + .hero-label-row { + display: none; + } + /* =========================== + Neighbours & organizations + =========================== */ + +/* Longer page + more breathing room */ +.landing-section { + margin-top: 3.2rem; + padding-bottom: 2rem; +} + +.landing-section-title { + font-size: 1.05rem; + font-weight: 600; + color: #e5e7eb; + margin-bottom: 1.1rem; +} + +/* Wide card layout with bigger visual */ +.landing-section-wide { + border-radius: 1.3rem; + background: rgba(15, 23, 42, 0.96); + border: 1px solid rgba(148, 163, 184, 0.6); + padding: 1.4rem 1.6rem; + box-shadow: 0 20px 40px rgba(15, 23, 42, 0.85); + display: grid; + grid-template-columns: 260px minmax(0, 1fr); + gap: 1.5rem; + min-height: 240px; +} + +@media (max-width: 960px) { + .landing-section-wide { + grid-template-columns: minmax(0, 1fr); + padding: 1.2rem 1.1rem; + } +} + +/* Typography inside these sections */ +.landing-section-text p { + font-size: 0.95rem; + line-height: 1.7; + color: #e5e7eb; +} + +.landing-section-text p + p { + margin-top: 1rem; +} + +/* Base line-illustration block */ +.line-illustration { + position: relative; + border-radius: 1rem; + min-height: 210px; + border: 1px solid rgba(148, 163, 184, 0.7); + overflow: hidden; + background: + radial-gradient(circle at 0% 0%, rgba(96, 165, 250, 0.45) 0%, transparent 55%), + radial-gradient(circle at 100% 100%, rgba(45, 212, 191, 0.38) 0%, transparent 55%), + #020617; +} + +/* Subtle animated grid inside the illustration */ +.line-illustration::before { + content: ""; + position: absolute; + inset: 0; + background-image: linear-gradient( + to right, + rgba(148, 163, 184, 0.14) 1px, + transparent 1px + ), + linear-gradient( + to bottom, + rgba(148, 163, 184, 0.14) 1px, + transparent 1px + ); + background-size: 32px 32px; + opacity: 0.7; +} + +/* Slight hover lift */ +.landing-section-wide:hover .line-illustration { + transform: translateY(-2px); + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 24px 50px rgba(15, 23, 42, 0.95); +} + +/* Colour tint per section */ +.line-illustration-neighbours { + background: + radial-gradient(circle at 0% 0%, rgba(52, 211, 153, 0.45) 0%, transparent 55%), + radial-gradient(circle at 100% 100%, rgba(96, 165, 250, 0.5) 0%, transparent 55%), + #020617; +} + +.line-illustration-orgs { + background: + radial-gradient(circle at 0% 0%, rgba(59, 130, 246, 0.5) 0%, transparent 55%), + radial-gradient(circle at 100% 100%, rgba(250, 204, 21, 0.45) 0%, transparent 55%), + #020617; +} + +/* ========= Neighbours drawing: two people + path + bag ========= */ + +.line-person { + position: absolute; + bottom: 34px; + width: 28px; + height: 56px; + border-radius: 16px; + border: 2px solid rgba(209, 213, 219, 0.9); +} + +.line-person::before { + content: ""; + position: absolute; + top: -20px; + left: 4px; + width: 18px; + height: 18px; + border-radius: 999px; + border: 2px solid rgba(209, 213, 219, 0.95); +} + +.line-person-1 { + left: 30px; + animation: neighbourBob 3.6s ease-in-out infinite; +} + +.line-person-2 { + left: 72px; + height: 50px; + animation: neighbourBob 3.6s ease-in-out infinite; + animation-delay: 0.4s; +} + +/* Reusable bob animation */ +@keyframes neighbourBob { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-4px); + } +} + +/* Bag the neighbours are carrying towards the org */ +.line-bag { + position: absolute; + bottom: 28px; + right: 26px; + width: 36px; + height: 30px; + border-radius: 10px; + border: 2px solid rgba(56, 189, 248, 0.95); +} + +.line-bag::before { + content: ""; + position: absolute; + top: -10px; + left: 10px; + width: 16px; + height: 10px; + border-radius: 8px 8px 0 0; + border: 2px solid rgba(56, 189, 248, 0.95); + border-bottom: none; +} + +/* Dashed path where people walk */ +.line-dashed-path { + position: absolute; + left: 40px; + right: 46px; + bottom: 18px; + height: 0; + border-top: 2px dashed rgba(148, 163, 184, 0.8); + opacity: 0.8; +} + +/* ========= Organizations drawing: building + boxes + sparkles ========= */ + +.line-building { + position: absolute; + bottom: 28px; + left: 26px; + width: 64px; + height: 54px; + border-radius: 8px; + border: 2px solid rgba(209, 213, 219, 0.95); +} + +/* windows */ +.line-building::before, +.line-building::after { + content: ""; + position: absolute; + width: 12px; + height: 12px; + border-radius: 3px; + border: 1px solid rgba(156, 163, 175, 0.95); +} + +.line-building::before { + top: 12px; + left: 10px; +} + +.line-building::after { + top: 12px; + right: 10px; +} + +/* door */ +.line-building .door { + display: none; /* in case you want to extend later */ +} + +/* donation boxes */ +.line-box { + position: absolute; + width: 30px; + height: 22px; + border-radius: 4px; + bottom: 24px; + border: 2px solid rgba(96, 165, 250, 0.95); +} + +.line-box-1 { + right: 30px; +} + +.line-box-2 { + right: 68px; + bottom: 32px; + border-color: rgba(22, 163, 74, 0.95); +} + +/* little sparkles to suggest activity / impact */ +.line-sparkle { + position: absolute; + width: 10px; + height: 10px; + border-radius: 2px; + border: 2px solid rgba(250, 204, 21, 0.9); + transform: rotate(45deg); + animation: sparklePulse 2.6s ease-in-out infinite; +} + +.line-sparkle-1 { + top: 26px; + right: 40px; +} + +.line-sparkle-2 { + top: 40px; + right: 70px; + animation-delay: 0.6s; +} + +@keyframes sparklePulse { + 0%, + 100% { + opacity: 0.4; + transform: translateY(0) rotate(45deg) scale(0.9); + } + 50% { + opacity: 1; + transform: translateY(-4px) rotate(45deg) scale(1.1); + } +} + +/* Scroll reveal (if you’re using IntersectionObserver) */ +.reveal-section { + opacity: 0; + transform: translateY(26px); + transition: opacity 0.6s ease, transform 0.6s ease; +} + +.reveal-section.visible { + opacity: 1; + transform: translateY(0); +} +/* ============================ + MAP VIEW – POLISHED SHELL + ============================ */ + +/* Overall app shell when logged in */ +.app-root { + background: radial-gradient(circle at 0% 0%, #e0f2fe 0%, #eef2ff 30%, #f9fafb 70%); + color: #0f172a; +} + +/* Content frame under the header */ +.app-content { + max-width: 1440px; + margin: 0 auto; + padding: 1.5rem 1.75rem 2.25rem; + display: flex; + flex-direction: column; + gap: 1.1rem; +} + +/* Layout: sidebar + map */ +.layout { + margin-top: 0.75rem; + display: grid; + grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); + gap: 1.25rem; + align-items: stretch; +} + +@media (max-width: 960px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } +} + +/* ============================ + SIDEBAR + LIST + ============================ */ + +.sidebar { + background: #ffffff; + border-radius: 1.25rem; + padding: 1rem 1.1rem; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08); + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; +} + +.sidebar-title { + margin: 0 0 0.6rem; + font-size: 0.95rem; + font-weight: 600; + color: #111827; + display: flex; + align-items: baseline; + justify-content: space-between; +} + +.sidebar-count { + font-size: 0.8rem; + color: #6b7280; +} + +.sidebar-list { + display: flex; + flex-direction: column; + gap: 0.65rem; + max-height: calc(100vh - 240px); + overflow-y: auto; + padding-right: 0.2rem; + scroll-behavior: smooth; +} + +/* slim scrollbar on sidebar only */ +.sidebar-list::-webkit-scrollbar { + width: 6px; +} +.sidebar-list::-webkit-scrollbar-track { + background: transparent; +} +.sidebar-list::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 999px; +} + +/* ============================ + CHARITY CARD – AIRY + MODERN + ============================ */ + +.charity-card { + border-radius: 1rem; + border: 1px solid #e5e7eb; + padding: 0.9rem 1rem; + background: #ffffff; + cursor: pointer; + transition: + transform 0.15s ease, + box-shadow 0.15s ease, + border-color 0.15s ease, + background 0.15s ease; +} + +.charity-card:hover { + border-color: #c7d2fe; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12); + transform: translateY(-2px); +} + +.charity-card-selected { + border-color: #4f46e5; + background: #f5f3ff; + box-shadow: 0 14px 36px rgba(79, 70, 229, 0.25); +} + +.charity-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.charity-name { + margin: 0; + font-size: 0.98rem; + font-weight: 600; + color: #111827; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.charity-cause { + margin: 0.2rem 0; + font-size: 0.83rem; + color: #4b5563; +} + +.charity-description { + margin: 0.25rem 0 0.45rem; + font-size: 0.82rem; + color: #6b7280; +} + +.charity-meta { + display: flex; + justify-content: space-between; + gap: 0.5rem; + font-size: 0.78rem; + color: #6b7280; + margin-bottom: 0.45rem; +} + +.charity-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.charity-footer-actions { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + justify-content: flex-end; +} + +/* pills + buttons – softer */ +.pill { + padding: 0.18rem 0.55rem; + border-radius: 999px; + background: #eef2ff; + color: #4338ca; + font-size: 0.74rem; + font-weight: 500; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + border: none; + padding: 0.4rem 0.9rem; + font-size: 0.8rem; + font-weight: 500; + background: #4f46e5; + color: #ffffff; + text-decoration: none; + cursor: pointer; + white-space: nowrap; + box-shadow: 0 10px 22px rgba(79, 70, 229, 0.35); + transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease; +} + +.button:hover { + background: #4338ca; + box-shadow: 0 14px 30px rgba(67, 56, 202, 0.45); + transform: translateY(-1px); +} + +.button-ghost { + background: #f9fafb; + color: #374151; + border: 1px solid #e5e7eb; + box-shadow: none; +} + +.button-ghost:hover { + background: #e5e7eb; +} + +/* ============================ + MAP CONTAINER + ============================ */ + +.map-container { + background: #ffffff; + border-radius: 1.25rem; + border: 1px solid #e5e7eb; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08); + overflow: hidden; + min-height: 420px; + max-height: calc(100vh - 200px); + animation: fadeIn 0.35s ease; +} + +.map { + width: 100%; + height: 100%; +} + +/* Custom emoji markers already defined; just slightly soften */ +.marker-inner { + width: 46px; + height: 46px; + border-radius: 999px; + background: #ffffff; + border: 3px solid #4f46e5; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.6rem; + box-shadow: 0 12px 26px rgba(15, 23, 42, 0.35); + transform: translateY(-9px); + transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease; +} + +.marker-inner-selected { + transform: translateY(-13px) scale(1.12); + border-color: #f97316; + box-shadow: 0 18px 34px rgba(251, 146, 60, 0.55); +} + +/* ============================ + QUICK ACTIONS + IMPACT BAR + ============================ */ + +.quick-actions { + margin-bottom: 0.5rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem 0.75rem; +} + +.quick-actions-label { + font-size: 0.86rem; + font-weight: 600; + color: #374151; +} + +.quick-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.quick-action-pill { + border-radius: 999px; + border: 1px solid #e5e7eb; + background: #ffffff; + padding: 0.35rem 0.9rem; + font-size: 0.82rem; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.35rem; + box-shadow: 0 8px 22px rgba(148, 163, 184, 0.35); + transition: background 0.12s ease, border-color 0.12s ease, + transform 0.08s ease, box-shadow 0.12s ease; +} + +.quick-action-pill:hover { + background: #eef2ff; + border-color: #4f46e5; + box-shadow: 0 14px 28px rgba(129, 140, 248, 0.4); + transform: translateY(-1px); +} + +/* Impact summary pill bar */ +.impact-summary { + margin-top: 0.4rem; + margin-bottom: 0.6rem; + padding: 0.65rem 0.9rem; + border-radius: 0.95rem; + background: rgba(255, 255, 255, 0.9); + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.7rem; + font-size: 0.86rem; + border: 1px solid #e5e7eb; + box-shadow: 0 14px 25px rgba(15, 23, 42, 0.06); +} + +/* ============================ + LOCATION STATUS CHIP + ============================ */ + +.location-status { + margin-top: 0.4rem; + font-size: 0.82rem; + padding: 0.55rem 0.8rem; + border-radius: 999px; + background: #e0f2fe; + color: #1d4ed8; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.location-status.error { + background: #fef2f2; + color: #b91c1c; +} + +/* ============================ + ORG BANNER – STICKY FOOTER + ============================ */ + +.org-banner { + margin-top: 1.4rem; + padding: 0.85rem 1.1rem; + border-radius: 0.9rem; + background: linear-gradient(90deg, #f97316, #facc15); + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.7rem; + color: #111827; + box-shadow: 0 14px 30px rgba(249, 115, 22, 0.35); +} + +.org-banner-label { + font-size: 0.92rem; + font-weight: 700; +} + +.org-banner-body { + font-size: 0.82rem; +} + +.org-banner-button { + border-radius: 999px; + background: #111827; + color: #f9fafb; + padding: 0.45rem 0.95rem; + font-size: 0.82rem; + text-decoration: none; + font-weight: 500; + white-space: nowrap; + box-shadow: 0 10px 20px rgba(15, 23, 42, 0.45); +} + +.org-banner-button:hover { + background: #020617; +} +/* ========================================= + MAP PAGE – MATCH LANDING PAGE AESTHETIC + (applies only when theme is "dark") + ========================================= */ + + .app-root.theme-dark { + background: radial-gradient(circle at 0% 0%, #1d4ed8 0%, #020617 55%); + color: #e5e7eb; + } + + /* Main content shell */ + .app-root.theme-dark .app-content { + max-width: 1440px; + margin: 0 auto; + padding: 1.8rem 2.1rem 2.6rem; + display: flex; + flex-direction: column; + gap: 1.2rem; + } + + /* tighten vertical rhythm above the main grid */ + .app-root.theme-dark .quick-actions { + margin-top: 0.4rem; + } + + .app-root.theme-dark .filter-bar { + margin-top: 0.6rem; + } + + .app-root.theme-dark .impact-summary { + margin-top: 0.7rem; + } + + .app-root.theme-dark .layout { + margin-top: 1.1rem; + display: grid; + grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); + gap: 1.4rem; + } + + @media (max-width: 960px) { + .app-root.theme-dark .layout { + grid-template-columns: minmax(0, 1fr); + } + } + + /* ======================== + SIDEBAR + LIST PANEL + ======================== */ + + .app-root.theme-dark .sidebar { + background: rgba(15, 23, 42, 0.96); + border-radius: 1.3rem; + padding: 1rem 1.1rem; + border: 1px solid rgba(148, 163, 184, 0.55); + box-shadow: 0 22px 45px rgba(15, 23, 42, 0.9); + } + + .app-root.theme-dark .sidebar-title { + font-size: 0.95rem; + font-weight: 600; + color: #f9fafb; + margin-bottom: 0.55rem; + } + + .app-root.theme-dark .sidebar-count { + font-size: 0.8rem; + color: #9ca3af; + } + + .app-root.theme-dark .sidebar-list { + max-height: calc(100vh - 250px); + padding-right: 0.25rem; + } + + /* slim scrollbar */ + .app-root.theme-dark .sidebar-list::-webkit-scrollbar { + width: 6px; + } + .app-root.theme-dark .sidebar-list::-webkit-scrollbar-track { + background: transparent; + } + .app-root.theme-dark .sidebar-list::-webkit-scrollbar-thumb { + background: #4b5563; + border-radius: 999px; + } + + /* ======================== + CHARITY CARDS + ======================== */ + + .app-root.theme-dark .charity-card { + border-radius: 1.05rem; + border: 1px solid rgba(31, 41, 55, 0.9); + background: radial-gradient(circle at 0% 0%, #020617 0%, #020617 45%, #020617 100%); + padding: 0.9rem 1rem; + transition: + transform 0.15s ease, + box-shadow 0.15s ease, + border-color 0.15s ease, + background 0.15s ease; + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.85); + } + + .app-root.theme-dark .charity-card:hover { + border-color: #4f46e5; + box-shadow: 0 18px 40px rgba(56, 189, 248, 0.55); + transform: translateY(-2px); + } + + .app-root.theme-dark .charity-card-selected { + border-color: #60a5fa; + background: radial-gradient(circle at 0% 0%, #1d4ed8 0%, #020617 60%); + } + + .app-root.theme-dark .charity-name { + color: #f9fafb; + } + + .app-root.theme-dark .charity-cause { + color: #9ca3af; + } + + .app-root.theme-dark .charity-description { + color: #9ca3af; + } + + .app-root.theme-dark .charity-meta { + color: #9ca3af; + } + + .app-root.theme-dark .charity-impact { + color: #e5e7eb; + } + + /* pill + buttons in dark mode */ + + .app-root.theme-dark .pill { + background: rgba(37, 99, 235, 0.16); + border-radius: 999px; + color: #bfdbfe; + border: 1px solid rgba(129, 140, 248, 0.6); + } + + .app-root.theme-dark .button { + background: linear-gradient(135deg, #4f46e5, #2563eb); + color: #f9fafb; + box-shadow: 0 16px 34px rgba(37, 99, 235, 0.6); + } + + .app-root.theme-dark .button:hover { + background: linear-gradient(135deg, #4338ca, #1d4ed8); + } + + /* ghost button ("Open in Maps") */ + .app-root.theme-dark .button-ghost { + background: rgba(15, 23, 42, 0.9); + color: #e5e7eb; + border: 1px solid rgba(148, 163, 184, 0.7); + box-shadow: none; + } + + .app-root.theme-dark .button-ghost:hover { + background: rgba(31, 41, 55, 0.9); + } + + /* ======================== + MAP CONTAINER + ======================== */ + + .app-root.theme-dark .map-container { + background: radial-gradient(circle at 0% 0%, #0f172a 0%, #020617 60%); + border-radius: 1.4rem; + border: 1px solid rgba(148, 163, 184, 0.55); + box-shadow: 0 25px 55px rgba(15, 23, 42, 0.95); + overflow: hidden; + min-height: 440px; + max-height: calc(100vh - 210px); + } + + .app-root.theme-dark .map { + width: 100%; + height: 100%; + } + + /* emoji markers – tuned to match landing neon vibe */ + .app-root.theme-dark .marker-inner { + width: 46px; + height: 46px; + border-radius: 999px; + background: #020617; + border: 3px solid #60a5fa; + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.9); + font-size: 1.6rem; + } + + .app-root.theme-dark .marker-inner-selected { + border-color: #f97316; + box-shadow: 0 20px 40px rgba(248, 250, 252, 0.4); + } + + /* ======================== + QUICK ACTIONS + FILTER BAR + ======================== */ + + .app-root.theme-dark .quick-actions-label { + color: #e5e7eb; + } + + .app-root.theme-dark .quick-action-pill { + background: rgba(15, 23, 42, 0.95); + border: 1px solid rgba(148, 163, 184, 0.7); + color: #e5e7eb; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.9); + } + + .app-root.theme-dark .quick-action-pill:hover { + background: rgba(37, 99, 235, 0.22); + border-color: #60a5fa; + } + + /* Filter selects */ + .app-root.theme-dark .filter-group label { + color: #e5e7eb; + } + + .app-root.theme-dark .filter-group select { + background: rgba(15, 23, 42, 0.96); + border: 1px solid rgba(148, 163, 184, 0.8); + color: #f9fafb; + } + + /* ======================== + IMPACT SUMMARY BAR + ======================== */ + + .app-root.theme-dark .impact-summary { + padding: 0.7rem 0.95rem; + border-radius: 1rem; + background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(148, 163, 184, 0.6); + box-shadow: 0 16px 40px rgba(15, 23, 42, 0.95); + font-size: 0.85rem; + } + + /* ======================== + LOCATION STATUS CHIP + ======================== */ + + .app-root.theme-dark .location-status { + background: rgba(15, 23, 42, 0.95); + color: #93c5fd; + border-radius: 999px; + border: 1px solid rgba(59, 130, 246, 0.7); + } + + .app-root.theme-dark .location-status.error { + background: rgba(127, 29, 29, 0.95); + color: #fee2e2; + } + + /* ======================== + ORG BANNER – DARK GLASS + ======================== */ + + .app-root.theme-dark .org-banner { + margin-top: 1.6rem; + padding: 0.9rem 1.1rem; + border-radius: 1rem; + background: linear-gradient(90deg, #f97316, #facc15); + color: #111827; + box-shadow: 0 20px 45px rgba(248, 181, 32, 0.6); + } + + .app-root.theme-dark .org-banner-button { + background: #111827; + color: #f9fafb; + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.9); + } +/* ========================================= + CLEAN DARK THEME LAYOUT + BACKGROUND + ========================================= */ + +/* Softer, centered gradient instead of bright bar on the far left */ +.app-root.theme-dark { + background: radial-gradient( + circle at 50% -20%, + #1e40af 0%, + #020617 55% + ); + color: #e5e7eb; +} + +/* Make main content span the full width, no “floaty” middle card */ +.app-root.theme-dark .app-content { + max-width: 100%; + margin: 0; + padding: 1.6rem 1.8rem 2.4rem; + box-sizing: border-box; +} + +/* Make header align visually with content padding */ +.theme-dark .header { + padding-inline: 1.8rem; +} + +/* Slightly tighten vertical spacing so the layout feels intentional */ +.app-root.theme-dark .quick-actions { + margin-top: 0.6rem; +} + +.app-root.theme-dark .filter-bar { + margin-top: 0.7rem; +} + +.app-root.theme-dark .impact-summary { + margin-top: 0.8rem; +} + +/* Keep the main grid strong and centered but not “boxed in” */ +.app-root.theme-dark .layout { + margin-top: 1.2rem; + display: grid; + grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); + gap: 1.4rem; +} +@media (max-width: 960px) { + .app-root.theme-dark .layout { + grid-template-columns: minmax(0, 1fr); + } +} + /* ================================ + MAP PAGE – SHARED LAYOUT + THEME + ================================ */ + +/* Page background matches landing page palette */ +.app-root { + min-height: 100vh; + display: flex; + flex-direction: column; + background: radial-gradient(circle at 0% 0%, #e0f2fe 0%, #dbeafe 24%, #020617 100%); + color: #0f172a; +} + +/* Dark mode version of the same layout */ +.app-root.theme-dark { + background: radial-gradient(circle at 0% 0%, #111827 0%, #020617 60%); + color: #e5e7eb; +} + +/* Main shell around filters + map */ +.app-content { + flex: 1; + max-width: 1200px; + margin: 0 auto 2.5rem; + padding: 1.6rem 2rem 2.6rem; +} + +/* Give everything a soft card look on both themes */ +.app-root:not(.theme-dark) .app-content, +.app-root.theme-dark .app-content { + background: rgba(248, 250, 252, 0.94); + border-radius: 1.5rem 1.5rem 1.2rem 1.2rem; + box-shadow: 0 26px 60px rgba(15, 23, 42, 0.25); +} + +/* In dark mode, card is darker but same shape */ +.app-root.theme-dark .app-content { + background: linear-gradient(145deg, #020617, #020617 55%, #020617 100%); +} + +/* Top “I have…” row + filters get a bit more breathing room */ +.filter-bar { + margin-top: 0.75rem; +} + +.summary-bar { + margin-top: 1rem; +} + +/* ================================ + LAYOUT: SIDEBAR + MAP + ================================ */ + +.layout { + margin-top: 1.6rem; + display: grid; + grid-template-columns: minmax(320px, 360px) minmax(0, 1.6fr); + gap: 1.5rem; + align-items: stretch; +} + +@media (max-width: 960px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } +} + +/* Sidebar card matches landing-page style */ +.sidebar { + border-radius: 1.2rem; + padding: 1rem; + background: #ffffff; + border: 1px solid #e5e7eb; + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12); +} + +.app-root.theme-dark .sidebar { + background: #020617; + border-color: #1f2937; + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5); +} + +/* Bigger map, fills its column cleanly */ +.map-container { + border-radius: 1.3rem; + overflow: hidden; + background: #ffffff; + border: 1px solid #e5e7eb; + box-shadow: 0 22px 50px rgba(15, 23, 42, 0.18); + min-height: 520px; + height: calc(100vh - 260px); +} + +.map { + width: 100%; + height: 100%; +} + +.app-root.theme-dark .map-container { + background: #020617; + border-color: #1f2937; +} + +/* Make sure the Leaflet container always stretches */ +.leaflet-container { + width: 100%; + height: 100%; +} + +/* ================================ + LIGHT / DARK COMPONENT TWEAKS + ================================ */ + +.app-root.theme-dark .summary-bar { + background: #020617; + border-radius: 0.85rem; + color: #e5e7eb; +} + +/* Charity cards stay the same layout across themes */ +.charity-card { + border-radius: 1rem; + border: 1px solid #e5e7eb; + background: #ffffff; +} + +.app-root.theme-dark .charity-card { + background: #020617; + border-color: #1f2937; +} + +.app-root.theme-dark .charity-card-selected { + background: #020617; +} + +/* Quick “I have…” pills use same shape on both themes */ +.quick-action-pill, +.button, +.points-display, +.theme-toggle { + border-radius: 999px; +} + +/* Small tightening for header row so it lines up with landing */ +.header { + padding: 1.2rem 2rem; + background: transparent; + box-shadow: none; +} +.app-root.theme-dark .header { + background: transparent; + border-bottom: none; +} + +/* Dark/light impact bar text contrast */ +.app-root:not(.theme-dark) .summary-bar { + background: #f3f4f6; +} +/* ====================================== + Better full-screen use + bigger map + ====================================== */ + +/* Let the main card span more of the viewport */ +.app-content { + max-width: 1440px; /* wider than before */ + width: 100%; + margin: 0 auto 2.8rem; + padding: 1.6rem 3rem 3rem; +} + +@media (max-width: 1200px) { + .app-content { + padding-inline: 1.8rem; + } +} + +@media (max-width: 768px) { + .app-content { + padding-inline: 1.2rem; + padding-top: 1.2rem; + } +} + +/* Sidebar vs map: give the map more real estate */ +.layout { + margin-top: 1.6rem; + display: grid; + grid-template-columns: minmax(320px, 380px) minmax(0, 2.3fr); + gap: 1.6rem; + align-items: stretch; +} + +@media (max-width: 960px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } +} + +/* Map should feel like the hero of the page */ +.map-container { + border-radius: 1.3rem; + overflow: hidden; + background: #ffffff; + border: 1px solid #e5e7eb; + box-shadow: 0 22px 50px rgba(15, 23, 42, 0.18); + + /* taller + closer to full viewport height */ + min-height: 640px; + height: calc(100vh - 220px); +} + +.app-root.theme-dark .map-container { + background: #020617; + border-color: #1f2937; +} + +/* Make sure Leaflet itself truly fills the container */ +.leaflet-container { + width: 100% !important; + height: 100% !important; +} + +/* ====================================== + Top “I have…” buttons & filters layout + ====================================== */ + +/* Spread pills nicely across the width */ +.quick-actions { + margin-top: 0.4rem; + margin-bottom: 0.8rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.6rem 0.9rem; +} + +.quick-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.6rem 0.9rem; +} + +.filter-bar { + margin-top: 0.4rem; + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 0.8rem 1.4rem; +} + +/* Slightly larger controls to feel more “hero” on big screens */ +.filter-group select { + min-width: 150px; +} + +@media (max-width: 768px) { + .filter-group select { + min-width: 0; + width: 100%; + } +} + +/* Keep the impact bar readable but compact */ +.summary-bar { + margin-top: 1.1rem; +} +/* ========================================= + OVERRIDES: BLUE THEME + STRONG SELECTION + (paste this at the very bottom of styles.css) + ========================================= */ + +/* ---------- Header + subtitle (light mode) ---------- */ + +/* Light mode header = soft blue bar, darker text for readability */ +.app-root:not(.theme-dark) .header { + background: linear-gradient(90deg, #e0f2fe, #dbeafe); + color: #0f172a; + border-bottom: 1px solid rgba(191, 219, 254, 0.8); +} + +/* Tagline under "NeighborGood" – make it readable and on-brand blue */ +.app-root:not(.theme-dark) .subtitle { + color: #1e3a8a; + font-weight: 500; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8); +} + +/* Dark mode header stays deep navy */ +.app-root.theme-dark .header { + background: linear-gradient(90deg, #020617, #020617); + border-bottom: 1px solid #0b1120; +} + +/* ---------- Layout + map: make better use of the screen ---------- */ + +/* Wider content area that takes advantage of the full viewport */ +.app-content { + max-width: 1440px; + width: 100%; + margin: 0 auto 2.8rem; + padding: 1.6rem 3rem 3rem; + box-sizing: border-box; +} + +@media (max-width: 1200px) { + .app-content { + padding-inline: 1.8rem; + } +} + +@media (max-width: 768px) { + .app-content { + padding-inline: 1.2rem; + padding-top: 1.2rem; + } +} + +/* Sidebar vs map: give the map more space */ +.layout { + margin-top: 1.6rem; + display: grid; + grid-template-columns: minmax(320px, 380px) minmax(0, 2.3fr); + gap: 1.6rem; + align-items: stretch; +} + +@media (max-width: 960px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } +} + +/* Map is now the hero of the page */ +.map-container { + border-radius: 1.3rem; + overflow: hidden; + background: #ffffff; + border: 1px solid #e5e7eb; + box-shadow: 0 22px 50px rgba(15, 23, 42, 0.18); + + /* closer to full viewport height */ + min-height: 640px; + height: calc(100vh - 220px); +} + +.app-root.theme-dark .map-container { + background: #020617; + border-color: #1f2937; +} + +/* Ensure Leaflet always fills */ +.leaflet-container, +.map { + width: 100% !important; + height: 100% !important; +} + +/* ---------- Quick actions + filters: better spread ---------- */ + +.quick-actions { + margin-top: 0.4rem; + margin-bottom: 0.8rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.6rem 0.9rem; +} + +.quick-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.6rem 0.9rem; +} + +.filter-bar { + margin-top: 0.4rem; + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 0.8rem 1.4rem; +} + +.filter-group select { + min-width: 150px; +} + +@media (max-width: 768px) { + .filter-group select { + min-width: 0; + width: 100%; + } +} + +/* Impact bar stays compact but readable */ +.summary-bar { + margin-top: 1.1rem; +} + +/* ---------- STRONGER BLUE SELECTION FOR CHARITIES (LIGHT MODE) ---------- */ + +.app-root:not(.theme-dark) .charity-card { + border-radius: 1rem; + border: 1px solid #e5e7eb; + background: #ffffff; + transition: + transform 0.15s ease, + box-shadow 0.15s ease, + border-color 0.15s ease, + background 0.15s ease; +} + +/* Selected card: bright blue, obvious, matches brand */ +.app-root:not(.theme-dark) .charity-card-selected { + border-color: #2563eb; + background: radial-gradient(circle at 0 0, #eff6ff 0, #ffffff 55%); + box-shadow: 0 16px 40px rgba(37, 99, 235, 0.35); +} + +.app-root:not(.theme-dark) .charity-card-selected .charity-name { + color: #1d4ed8; +} + +.app-root:not(.theme-dark) .charity-card-selected .urgency-tag { + border-radius: 999px; + border: 1px solid rgba(59, 130, 246, 0.25); +} + +/* CTA in selected card gets the same blue/purple gradient as landing CTA */ +.app-root:not(.theme-dark) .charity-card-selected .button { + background: linear-gradient(135deg, #4f46e5, #2563eb); + box-shadow: 0 12px 26px rgba(37, 99, 235, 0.5); +} + +.app-root:not(.theme-dark) .charity-card-selected .button:hover { + background: linear-gradient(135deg, #4338ca, #1d4ed8); +} + +/* ---------- Map markers: stronger selection ring ---------- */ + +.marker-inner { + width: 46px; + height: 46px; + border-radius: 999px; + background: #ffffff; + border: 3px solid #4f46e5; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.6rem; + box-shadow: 0 12px 26px rgba(15, 23, 42, 0.35); + transform: translateY(-9px); + transition: + transform 0.12s ease, + box-shadow 0.12s ease, + border-color 0.12s ease; +} + +/* Hover for any marker */ +.marker-inner:hover { + transform: translateY(-11px) scale(1.05); + box-shadow: 0 16px 34px rgba(15, 23, 42, 0.5); +} + +/* Selected marker: bright blue glow */ +.marker-inner-selected { + transform: translateY(-13px) scale(1.12); + border-color: #2563eb; + box-shadow: 0 18px 42px rgba(37, 99, 235, 0.8); +} + +/* Dark mode version: neon blue ring on navy */ +.app-root.theme-dark .marker-inner { + background: #020617; + border-color: #60a5fa; + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.9); +} + +.app-root.theme-dark .marker-inner-selected { + border-color: #f97316; + box-shadow: 0 20px 40px rgba(248, 181, 32, 0.7); +} + +/* ---------- Impact + session summary in light mode ---------- */ + +.app-root:not(.theme-dark) .impact-summary, +.app-root:not(.theme-dark) .summary-bar { + background: rgba(255, 255, 255, 0.96); + border-radius: 0.95rem; + border: 1px solid #e5e7eb; + box-shadow: 0 14px 25px rgba(15, 23, 42, 0.06); +} + +/* ---------- Quick actions: subtle blue theme ---------- */ + +.app-root:not(.theme-dark) .quick-action-pill { + border-radius: 999px; + border: 1px solid #e5e7eb; + background: #ffffff; + padding: 0.35rem 0.9rem; + font-size: 0.82rem; + display: inline-flex; + align-items: center; + gap: 0.35rem; + box-shadow: 0 8px 22px rgba(148, 163, 184, 0.35); + transition: + background 0.12s ease, + border-color 0.12s ease, + transform 0.08s ease, + box-shadow 0.12s ease; +} + +.app-root:not(.theme-dark) .quick-action-pill:hover { + background: #eef2ff; + border-color: #4f46e5; + box-shadow: 0 14px 28px rgba(129, 140, 248, 0.4); + transform: translateY(-1px); +} + +/* Optional “active” state if you add a class in JS */ +.app-root:not(.theme-dark) .quick-action-pill.quick-action-pill--active { + background: linear-gradient(135deg, #4f46e5, #2563eb); + color: #f9fafb; + border-color: transparent; + box-shadow: 0 18px 38px rgba(37, 99, 235, 0.7); +} +/* ======================================================= + OVERRIDES – put this at the VERY BOTTOM of styles.css + ======================================================= */ + +/* 1) Fix body / app background so there is no weird grey bar */ +html, +body { + margin: 0; + height: 100%; + background: radial-gradient(circle at 0% 0%, #e0f2fe 0%, #dbeafe 28%, #020617 100%); +} + +.app-root { + min-height: 100vh; +} + +/* 2) HackCamp pill – readable in LIGHT + DARK */ + +.header-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.4rem; +} + +.header-badge { + display: inline-flex; + align-items: center; + justify-content: center; + max-width: 420px; + padding: 0.4rem 1rem; + border-radius: 999px; + font-size: 0.82rem; + font-weight: 500; + white-space: nowrap; +} + +/* light mode pill */ +.app-root:not(.theme-dark) .header-badge { + background: #0b1120; /* deep navy */ + border: 1px solid #1f2937; + color: #e5e7eb; /* light text so it pops */ + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.25); +} + +/* dark mode pill */ +.app-root.theme-dark .header-badge { + background: rgba(15, 23, 42, 0.95); + border: 1px solid #374151; + color: #e5e7eb; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.7); +} + +/* 3) Stronger blue highlight when a charity is selected */ + +.charity-card-selected { + border-color: #2563eb; + background: linear-gradient(135deg, #eff6ff, #ffffff); + box-shadow: + 0 0 0 1px #bfdbfe, + 0 18px 40px rgba(37, 99, 235, 0.35); +} + +/* dark-mode selected card */ +.app-root.theme-dark .charity-card-selected { + border-color: #60a5fa; + background: radial-gradient(circle at 0% 0%, #1d4ed8 0%, #020617 65%); + box-shadow: + 0 0 0 1px rgba(96, 165, 250, 0.6), + 0 20px 44px rgba(15, 23, 42, 0.9); +} + +/* make the title in selected card a bit more vivid in light mode */ +.app-root:not(.theme-dark) .charity-card-selected .charity-name { + color: #1d4ed8; +} + + diff --git a/src/utils/distance.js b/src/utils/distance.js new file mode 100644 index 0000000..3ca7d5e --- /dev/null +++ b/src/utils/distance.js @@ -0,0 +1,41 @@ +const R = 6371; // Earth radius in km + +function toRad(value) { + return (value * Math.PI) / 180; +} + +export function distanceInKmBetweenCoords(lat1, lon1, lat2, lon2) { + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * + Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + +export function addDistanceToCharities(charities, userLocation) { + if (!userLocation) { + return charities.map((c) => ({ ...c, distanceKm: null })); + } + + const { lat, lng } = userLocation; + + return charities.map((c) => { + const distanceKm = distanceInKmBetweenCoords( + lat, + lng, + c.latitude, + c.longitude + ); + return { + ...c, + distanceKm: Math.round(distanceKm * 10) / 10, + }; + }); +}