Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
50166ee
fix: stabilize map location and loading behavior
kckagancan May 11, 2026
c09f080
fix: preserve initial help map marker fit
kckagancan May 11, 2026
1137ea7
fix: fit initial help markers without stale center
kckagancan May 11, 2026
301f71a
feat: add bogazici final seed demo data
kckagancan May 11, 2026
91f9232
fix: rearrange the location of ucaksavar request
kckagancan May 11, 2026
d710dbd
Merge pull request #599 from bounswe/fix/map-location-behavior
erinc00 May 12, 2026
07b2650
doc: prepare submision documents
kckagancan May 12, 2026
b7267ae
doc: improve README file
kckagancan May 12, 2026
897abe8
fix: assignment overwrite risk
kckagancan May 12, 2026
bb137db
fix: safe assignment insert
kckagancan May 12, 2026
e7a9472
fix: conflict handling
kckagancan May 12, 2026
88ed627
Merge pull request #601 from bounswe/feat/add-demo-data
erinc00 May 12, 2026
5cf0587
Merge branch 'development' into doc/prepare-setup-docs
kckagancan May 12, 2026
2a76528
doc: add new demo data into README
kckagancan May 12, 2026
6f0b8c4
fix: replace placeholder url with neph url
kckagancan May 12, 2026
d5cfd9f
Merge pull request #603 from bounswe/doc/prepare-setup-docs
erinc00 May 12, 2026
b70c60a
fix(android): navigate after edited help request submission
kckagancan May 12, 2026
891d348
Merge pull request #604 from bounswe/fix/request-help-navigation
mehmetcangurbuz08 May 12, 2026
cd34696
fix(android-news): load announcements from backend and add detail screen
erinc00 May 12, 2026
78f469a
fix(web-news): remove demo fallback content and unify open announceme…
erinc00 May 12, 2026
23fa5c8
fix(web): force en-US timestamp formatting
erinc00 May 12, 2026
8d04508
test(news): align fallback e2e with explicit-error behavior
erinc00 May 12, 2026
d806e3c
Merge pull request #605 from bounswe/fix/news-announcements-backend-p…
kckagancan May 12, 2026
f66dc64
fix(android-auth): replace heart icon with app logo on login/signup
erinc00 May 12, 2026
0ceb172
fix(android-auth): use app logo on welcome screen
erinc00 May 12, 2026
5236457
Merge pull request #607 from bounswe/fix/android-auth-logo-replace-he…
kckagancan May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 94 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ For Android development:
- Android Studio
- Android SDK compatible with the project

## Live deployment

- Web app: `https://neph.app/`
- Backend API: `https://api.neph.app/api`

## Environment variables

Keep secrets and real credentials out of the repository. Use local env files or shell overrides.
Expand Down Expand Up @@ -105,6 +110,7 @@ Notes:
- Email-based flows require valid SMTP credentials.
- Placeholder SMTP values in the example file are not usable as-is.
- In Docker Compose, the backend connects to PostgreSQL through the internal service name `postgres`.
- Production deployments must use real database credentials and secure secrets. `JWT_SECRET` must not remain at the default value in production.

### Web

Expand All @@ -119,8 +125,10 @@ Important variables include:

Notes:

- `NEXT_PUBLIC_API_BASE_URL` is used for browser-facing requests.
- `API_BASE_URL` can be used for server-side requests inside the Docker network.
- `NEXT_PUBLIC_API_BASE_URL` is the browser/client-side API base.
- `API_BASE_URL` is the server-side Next.js rewrite target.
- `/api` works for Docker and Next.js rewrite usage.
- `http://localhost:3000/api` works for direct backend access outside the rewrite path.

### Docker Compose overrides

Expand Down Expand Up @@ -169,9 +177,76 @@ Notes:
- the backend waits for PostgreSQL to become healthy before starting
- the web container is configured to talk to the backend in the local Docker setup

### Optional demo data seed
### Backend setup

To run the backend directly:

```bash
cd backend
cp .env.example .env
npm install
npm run migrate
npm start
```

Use real database credentials and secure secrets for production. `JWT_SECRET` must not remain at the default value in production.

Demo-ready data is stored as SQL seed migrations under `backend/demo-migrations/`, but it is not applied by the normal backend migration flow. This keeps regular `npm run migrate` and `start:with-migrations` safe for normal environments.
### Web setup

To run the web app directly:

```bash
cd web
cp .env.example .env.local
npm install
npm run dev
npm run build
npm run start
```

Use `NEXT_PUBLIC_API_BASE_URL=/api` with the Next.js rewrite, including Docker. Use `NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api` when the browser should call the backend directly.

### Mobile setup

To build the Android app:

```bash
cd android
cp .env.example .env
cp keystore.properties.example keystore.properties
./gradlew :app:assembleDebug
./gradlew :app:assembleRelease
```

Debug Android emulator builds use `http://10.0.2.2:3000/api` to reach the host backend. Do not use `localhost` from inside the emulator for the host backend. Release builds use `NEPH_RELEASE_API_BASE_URL`, defaulting to `https://api.neph.app/api`.

`android/.env.example` is included for submission documentation. Local Gradle builds read release values from `keystore.properties`, environment variables, or Gradle properties. Before running `./gradlew :app:assembleRelease`, replace the placeholder values in `keystore.properties` with real signing values and make sure the keystore file exists at `ANDROID_KEYSTORE_PATH`. Do not include real secrets in the repository.

The signed release APK must be attached to the GitHub Release manually or by the release workflow.

### Final Boğaziçi demo data

Final demo data is stored in the normal backend migration flow:

- `backend/migrations/20260512_120000__seed_bogazici_final_demo_data.sql`

It is applied by the normal backend migration runner. In Docker Compose, backend migrations run automatically on startup, so no separate `ENABLE_DEMO_SEED=true npm run seed:demo` command is needed for the Boğaziçi final demo data after migrations run. The old `backend/demo-migrations/` flow is optional legacy demo data only.

Boğaziçi final demo accounts:

| Email | Password |
| --- | --- |
| `bogazici_reserve_1@neph.test` | `DemoPass123!` |
| `bogazici_reserve_2@neph.test` | `DemoPass123!` |
| `bogazici_reserve_3@neph.test` | `DemoPass123!` |
| `bogazici_reserve_4@neph.test` | `DemoPass123!` |
| `bogazici_assigned_1@neph.test` | `DemoPass123!` |
| `bogazici_assigned_2@neph.test` | `DemoPass123!` |
| `bogazici_requester_new_hall@neph.test` | `DemoPass123!` |

### Optional legacy demo data seed

Additional legacy demo-ready data is stored as SQL seed migrations under `backend/demo-migrations/`, but it is not applied by the normal backend migration flow. This keeps regular `npm run migrate` and `start:with-migrations` safe for normal environments.

To apply the demo dataset intentionally, run the regular migrations first and then run the guarded demo seed command:

Expand Down Expand Up @@ -237,17 +312,26 @@ Relevant files:
- `web/Dockerfile`
- `infra/docker/postgres/init.sql`

## Mobile note
## Production setup

Production deployment requires backend production environment values for database credentials or `DATABASE_URL`, a secure `JWT_SECRET`, SMTP values if email flows are used, Google OAuth values if Google Sign-In is used, and push notification values if push delivery is enabled. It also requires web production values for `NEXT_PUBLIC_API_BASE_URL` and `API_BASE_URL`.

The Android application lives in `android/`.
Use secure secrets instead of demo/default values. Start the backend with the normal migration-enabled startup flow so SQL migrations are applied before serving traffic.

- local debug builds are expected to reach the backend through the emulator bridge at `http://10.0.2.2:3000`
- Android is not part of the root Docker Compose workflow
- mobile build and release steps are handled separately from the local Docker setup
Before final release, verify the backend health endpoint, the backend API base at `https://api.neph.app/api`, and the deployed web URL. The signed Android APK must be attached to the GitHub Release.

## Deployment note

This README focuses on local development and evaluation. Deployment- and release-related files may exist elsewhere in the repository, but the top-level guide is intentionally centered on how to build and run the project locally.
The deployed backend API base is `https://api.neph.app/api`. The deployed web URL must be filled into this README and the GitHub Release notes before final submission if it is not already known.

## Final release checklist

- Code is merged to `main`.
- Tag `final-milestone` points to the final `main` commit.
- GitHub Release name is `1.0.0`.
- GitHub Release is official, not a pre-release.
- Release notes include the deployed web URL.
- Signed APK is attached to the GitHub Release.

## Development notes

Expand Down
7 changes: 7 additions & 0 deletions android/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NEPH_RELEASE_API_BASE_URL=https://api.neph.app/api
GOOGLE_SERVER_CLIENT_ID=YOUR_GOOGLE_SERVER_CLIENT_ID

ANDROID_KEYSTORE_PATH=release.keystore
ANDROID_KEYSTORE_PASSWORD=YOUR_KEYSTORE_PASSWORD
ANDROID_KEY_ALIAS=YOUR_KEY_ALIAS
ANDROID_KEY_PASSWORD=YOUR_KEY_PASSWORD
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import com.neph.features.auth.util.isValidEmail
import com.neph.ui.components.buttons.PrimaryButton
import com.neph.ui.components.buttons.SecondaryButton
import com.neph.ui.components.buttons.TextActionButton
import com.neph.ui.components.display.BrandLogo
import com.neph.ui.components.display.AuthHeaderAppLogo
import com.neph.ui.components.display.Divider
import com.neph.ui.components.display.HelperText
import com.neph.ui.components.inputs.AppTextField
Expand Down Expand Up @@ -212,10 +212,7 @@ fun LoginScreen(
title = "Welcome back",
subtitle = "Log in to manage your emergency information and stay ready.",
logoContent = {
BrandLogo(
size = 64.dp,
showWordmark = false
)
AuthHeaderAppLogo(size = 64.dp)
},
footerContent = {
AuthFooterLinks(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import com.neph.features.auth.presentation.components.SocialAuthMode
import com.neph.features.auth.util.isValidEmail
import com.neph.ui.components.buttons.PrimaryButton
import com.neph.ui.components.buttons.SecondaryButton
import com.neph.ui.components.display.BrandLogo
import com.neph.ui.components.display.AuthHeaderAppLogo
import com.neph.ui.components.display.Divider
import com.neph.ui.components.display.HelperText
import com.neph.ui.components.inputs.AppTextField
Expand Down Expand Up @@ -260,10 +260,7 @@ fun SignupScreen(
title = "Create Account",
subtitle = "Set up your account and get ready before emergencies happen.",
logoContent = {
BrandLogo(
size = 64.dp,
showWordmark = false
)
AuthHeaderAppLogo(size = 64.dp)
},
footerContent = {
AuthFooterLinks(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.compose.ui.unit.dp
import com.neph.ui.components.buttons.PrimaryButton
import com.neph.ui.components.buttons.SecondaryButton
import com.neph.ui.components.buttons.TextActionButton
import com.neph.ui.components.display.BrandLogo
import com.neph.ui.components.display.AuthHeaderAppLogo
import com.neph.ui.layout.AuthScaffold
import com.neph.ui.theme.LocalNephSpacing

Expand All @@ -23,10 +23,7 @@ fun WelcomeScreen(
title = "Welcome to NEPH",
subtitle = "Prepare, connect, and stay ready with your neighborhood emergency hub.",
logoContent = {
BrandLogo(
size = 72.dp,
showWordmark = false
)
AuthHeaderAppLogo(size = 72.dp)
}
) {
Column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ fun GatheringAreasScreen(
activeFilterKeys.contains(item.category.trim().lowercase())
}.orEmpty()
val isFilterEmpty = currentResult != null && currentResult.areas.isNotEmpty() && visibleAreas.isEmpty()
val mapActionsEnabled = !loading && !backgroundUpdating

if (selectedAreaId != null && visibleAreas.none { it.id == selectedAreaId }) {
selectedAreaId = null
Expand Down Expand Up @@ -456,7 +457,7 @@ fun GatheringAreasScreen(
pendingViewport = viewport
viewportRefreshNonce += 1
},
enabled = !loading
enabled = mapActionsEnabled
)
}
}
Expand All @@ -477,7 +478,7 @@ fun GatheringAreasScreen(
loadingResources = loading,
updatingResources = backgroundUpdating,
onShowCurrentLocation = ::showCurrentLocationOnMap,
showCurrentLocationEnabled = !loading,
showCurrentLocationEnabled = mapActionsEnabled,
onViewportChanged = ::handleViewportChanged
)

Expand All @@ -499,7 +500,8 @@ fun GatheringAreasScreen(
onClick = {
pendingViewport = currentViewport
viewportRefreshNonce += 1
}
},
enabled = mapActionsEnabled
)
}
}
Expand All @@ -519,7 +521,8 @@ fun GatheringAreasScreen(
onClick = {
pendingViewport = currentViewport
viewportRefreshNonce += 1
}
},
enabled = mapActionsEnabled
)
}
}
Expand Down Expand Up @@ -663,7 +666,8 @@ fun GatheringAreasScreen(
onClick = {
pendingViewport = currentViewport
viewportRefreshNonce += 1
}
},
enabled = mapActionsEnabled
)
}
}
Expand Down Expand Up @@ -851,11 +855,11 @@ private fun GatheringAreasMapCard(
HelperText(text = ResourceLoadingMessage)
}

if (updatingResources && markers.isNotEmpty()) {
if (updatingResources) {
HelperText(text = ResourceUpdatingMessage)
}

if (markers.isEmpty() && !loadingResources) {
if (markers.isEmpty() && !loadingResources && !updatingResources) {
HelperText(
text = emptyMarkersMessage
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,18 @@ fun HelpRequestMapScreen(
var mapCenterLongitude by remember { mutableStateOf(TurkeyOverviewLongitude) }
var mapZoom by remember { mutableStateOf(TurkeyOverviewZoom) }
var mapResetNonce by remember { mutableStateOf(0) }
var initialMarkerFitApplied by remember { mutableStateOf(false) }
var markerFitBoundsToken by remember { mutableStateOf<Int?>(null) }

fun applyRequestResult(result: ActiveHelpRequestsResult, viewportKey: String?) {
if (
!initialMarkerFitApplied &&
viewportKey == null &&
result.requests.any { it.hasValidMapCoordinates() }
) {
markerFitBoundsToken = (markerFitBoundsToken ?: 0) + 1
initialMarkerFitApplied = true
}
requests = result.requests
viewportKey?.let { lastFetchedViewportKey = it }
viewportRefreshNonce = 0
Expand Down Expand Up @@ -300,6 +310,7 @@ fun HelpRequestMapScreen(
mapCenterLongitude = location.longitude
mapZoom = HelpRequestMapCurrentLocationZoom
mapResetNonce += 1
markerFitBoundsToken = null
selectedRequestId = null
lastFetchedViewportKey = null
loading = false
Expand Down Expand Up @@ -475,6 +486,7 @@ fun HelpRequestMapScreen(

val selectedRequest = visibleRequests.firstOrNull { it.requestId == selectedRequestId }
val isFilterEmpty = !loading && requests.isNotEmpty() && visibleRequests.isEmpty()
val mapActionsEnabled = !loading && !backgroundUpdating
val mapEmptyMarkersMessage = helpRequestMapEmptyMessage(
blockingLoading = loading,
errorMessage = errorMessage,
Expand Down Expand Up @@ -518,7 +530,7 @@ fun HelpRequestMapScreen(
SecondaryButton(
text = "Refresh Help Request Map",
onClick = { queueViewportRefresh() },
enabled = !loading
enabled = mapActionsEnabled
)
}
}
Expand All @@ -534,8 +546,9 @@ fun HelpRequestMapScreen(
mapCenterLongitude = mapCenterLongitude,
mapZoom = mapZoom,
mapResetToken = mapResetNonce,
fitBoundsRequestToken = markerFitBoundsToken,
onShowCurrentLocation = ::showCurrentLocationOnMap,
showCurrentLocationEnabled = !loading,
showCurrentLocationEnabled = mapActionsEnabled,
onViewportChanged = ::handleViewportChanged,
onSelectRequest = { selectedRequestId = it }
)
Expand All @@ -557,7 +570,8 @@ fun HelpRequestMapScreen(
)
SecondaryButton(
text = "Retry",
onClick = { queueViewportRefresh() }
onClick = { queueViewportRefresh() },
enabled = mapActionsEnabled
)
}
}
Expand Down Expand Up @@ -643,7 +657,8 @@ fun HelpRequestMapScreen(
HelperText(text = errorMessage)
SecondaryButton(
text = "Retry",
onClick = { queueViewportRefresh() }
onClick = { queueViewportRefresh() },
enabled = mapActionsEnabled
)
}
}
Expand Down Expand Up @@ -905,6 +920,7 @@ private fun CrisisRequestMapPanel(
mapCenterLongitude: Double = TurkeyOverviewLongitude,
mapZoom: Int = TurkeyOverviewZoom,
mapResetToken: Int = 0,
fitBoundsRequestToken: Int? = null,
onShowCurrentLocation: (() -> Unit)? = null,
showCurrentLocationEnabled: Boolean = true,
onViewportChanged: (LeafletMapViewport) -> Unit,
Expand All @@ -920,7 +936,7 @@ private fun CrisisRequestMapPanel(
val effectiveCenterLatitude = if (shouldFitRequestMarkers) mapInstanceKey.centerLatitude else mapCenterLatitude
val effectiveCenterLongitude = if (shouldFitRequestMarkers) mapInstanceKey.centerLongitude else mapCenterLongitude
val effectiveZoom = if (shouldFitRequestMarkers) 13 else mapZoom
val mapInstanceId = remember(mapResetToken, mapInstanceKey) {
val mapInstanceId = remember(mapResetToken) {
newLeafletMapInstanceId()
}
val currentMapInstanceIdState = remember { mutableStateOf(mapInstanceId) }
Expand Down Expand Up @@ -999,6 +1015,7 @@ private fun CrisisRequestMapPanel(
zoom = effectiveZoom,
showCenterMarker = false,
fitBoundsToMarkers = shouldFitRequestMarkers,
fitBoundsRequestToken = fitBoundsRequestToken,
onMarkerSelected = { markerInstanceId, markerId ->
if (markerInstanceId == currentMapInstanceIdState.value) {
onSelectRequest(markerId)
Expand Down Expand Up @@ -1052,11 +1069,11 @@ private fun CrisisRequestMapPanel(
HelperText(text = ResourceLoadingMessage)
}

if (updatingResources && markers.isNotEmpty()) {
if (updatingResources) {
HelperText(text = ResourceUpdatingMessage)
}

if (markers.isEmpty() && !loadingResources) {
if (markers.isEmpty() && !loadingResources && !updatingResources) {
HelperText(text = emptyMarkersMessage)
}

Expand Down
Loading
Loading