From a1f0518d0cd123a791adde64f4f11bd8e44276c7 Mon Sep 17 00:00:00 2001 From: ivar Date: Mon, 20 Oct 2025 00:26:34 +0200 Subject: Initial commit --- .gitignore | 120 +++++++ api/.dockerignore | 25 ++ api/.idea/.idea.WhatApi/.idea/.gitignore | 13 + .../.idea.WhatApi/.idea/dictionaries/project.xml | 7 + api/.idea/.idea.WhatApi/.idea/encodings.xml | 4 + api/.idea/.idea.WhatApi/.idea/indexLayout.xml | 8 + .../.idea.WhatApi/.idea/jsLibraryMappings.xml | 6 + api/WhatApi.sln | 16 + api/WhatApi/Constants.cs | 6 + api/WhatApi/Database.cs | 19 + api/WhatApi/Dockerfile | 23 ++ api/WhatApi/Endpoints/BaseEndpoint.cs | 12 + api/WhatApi/Endpoints/DownloadContentEndpoint.cs | 19 + api/WhatApi/Endpoints/GetPlacesEndpoint.cs | 57 +++ api/WhatApi/Endpoints/UploadContentEndpoint.cs | 49 +++ api/WhatApi/Middleware/AuthenticationMiddleware.cs | 8 + api/WhatApi/Middleware/AuthorizationMiddleware.cs | 8 + api/WhatApi/Middleware/UserLastSeenMiddleware.cs | 8 + .../Migrations/20251013213511_Initial.Designer.cs | 93 +++++ api/WhatApi/Migrations/20251013213511_Initial.cs | 75 ++++ api/WhatApi/Migrations/DatabaseModelSnapshot.cs | 90 +++++ api/WhatApi/Pages/Map.cshtml | 92 +++++ api/WhatApi/Pages/Map.cshtml.cs | 10 + api/WhatApi/Pages/Upload.cshtml | 82 +++++ api/WhatApi/Pages/Upload.cshtml.cs | 10 + api/WhatApi/Program.cs | 45 +++ api/WhatApi/Properties/launchSettings.json | 14 + api/WhatApi/Seed.cs | 24 ++ api/WhatApi/Tables/Content.cs | 12 + api/WhatApi/Tables/Place.cs | 11 + api/WhatApi/Tables/Session.cs | 10 + api/WhatApi/Tables/User.cs | 12 + api/WhatApi/WhatApi.csproj | 34 ++ api/WhatApi/appsettings.Development.json | 8 + api/WhatApi/appsettings.json | 9 + api/WhatApi/wwwroot/pin.png | Bin 0 -> 986 bytes ios/H--appen-Info.plist | 5 + "ios/H\303\246-appen.xcodeproj/project.pbxproj" | 381 +++++++++++++++++++++ .../project.xcworkspace/contents.xcworkspacedata" | 7 + .../xcshareddata/swiftpm/Package.resolved" | 15 + .../AccentColor.colorset/Contents.json" | 11 + .../AppIcon.appiconset/Contents.json" | 35 ++ .../H\303\246-appen/Assets.xcassets/Contents.json" | 6 + "ios/H\303\246-appen/ClusterHit.swift" | 18 + "ios/H\303\246-appen/HaeappenApp.swift" | 17 + "ios/H\303\246-appen/LocationAuthorizer.swift" | 20 ++ "ios/H\303\246-appen/MapContentView.swift" | 45 +++ "ios/H\303\246-appen/PostAnnotationView.swift" | 9 + "ios/H\303\246-appen/TabBarView.swift" | 22 ++ 49 files changed, 1630 insertions(+) create mode 100644 .gitignore create mode 100644 api/.dockerignore create mode 100644 api/.idea/.idea.WhatApi/.idea/.gitignore create mode 100644 api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml create mode 100644 api/.idea/.idea.WhatApi/.idea/encodings.xml create mode 100644 api/.idea/.idea.WhatApi/.idea/indexLayout.xml create mode 100644 api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml create mode 100644 api/WhatApi.sln create mode 100644 api/WhatApi/Constants.cs create mode 100644 api/WhatApi/Database.cs create mode 100644 api/WhatApi/Dockerfile create mode 100644 api/WhatApi/Endpoints/BaseEndpoint.cs create mode 100644 api/WhatApi/Endpoints/DownloadContentEndpoint.cs create mode 100644 api/WhatApi/Endpoints/GetPlacesEndpoint.cs create mode 100644 api/WhatApi/Endpoints/UploadContentEndpoint.cs create mode 100644 api/WhatApi/Middleware/AuthenticationMiddleware.cs create mode 100644 api/WhatApi/Middleware/AuthorizationMiddleware.cs create mode 100644 api/WhatApi/Middleware/UserLastSeenMiddleware.cs create mode 100644 api/WhatApi/Migrations/20251013213511_Initial.Designer.cs create mode 100644 api/WhatApi/Migrations/20251013213511_Initial.cs create mode 100644 api/WhatApi/Migrations/DatabaseModelSnapshot.cs create mode 100644 api/WhatApi/Pages/Map.cshtml create mode 100644 api/WhatApi/Pages/Map.cshtml.cs create mode 100644 api/WhatApi/Pages/Upload.cshtml create mode 100644 api/WhatApi/Pages/Upload.cshtml.cs create mode 100644 api/WhatApi/Program.cs create mode 100644 api/WhatApi/Properties/launchSettings.json create mode 100644 api/WhatApi/Seed.cs create mode 100644 api/WhatApi/Tables/Content.cs create mode 100644 api/WhatApi/Tables/Place.cs create mode 100644 api/WhatApi/Tables/Session.cs create mode 100644 api/WhatApi/Tables/User.cs create mode 100644 api/WhatApi/WhatApi.csproj create mode 100644 api/WhatApi/appsettings.Development.json create mode 100644 api/WhatApi/appsettings.json create mode 100644 api/WhatApi/wwwroot/pin.png create mode 100644 ios/H--appen-Info.plist create mode 100644 "ios/H\303\246-appen.xcodeproj/project.pbxproj" create mode 100644 "ios/H\303\246-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata" create mode 100644 "ios/H\303\246-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" create mode 100644 "ios/H\303\246-appen/Assets.xcassets/AccentColor.colorset/Contents.json" create mode 100644 "ios/H\303\246-appen/Assets.xcassets/AppIcon.appiconset/Contents.json" create mode 100644 "ios/H\303\246-appen/Assets.xcassets/Contents.json" create mode 100644 "ios/H\303\246-appen/ClusterHit.swift" create mode 100644 "ios/H\303\246-appen/HaeappenApp.swift" create mode 100644 "ios/H\303\246-appen/LocationAuthorizer.swift" create mode 100644 "ios/H\303\246-appen/MapContentView.swift" create mode 100644 "ios/H\303\246-appen/PostAnnotationView.swift" create mode 100644 "ios/H\303\246-appen/TabBarView.swift" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2e5fe9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output +i## A streamlined .gitignore for modern .NET projects +## including temporary files, build results, and +## files generated by popular .NET tools. If you are +## developing with Visual Studio, the VS .gitignore +## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## has more thorough IDE-specific entries. +## +## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg + +# Others +~$* +*~ +CodeCoverage/ + +# MSBuild Binary and Structured Log +*.binlog + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml +Bogus.Premium.LicenseKey +**/files/* +**/.DS_Store +**/*.user diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/api/.idea/.idea.WhatApi/.idea/.gitignore b/api/.idea/.idea.WhatApi/.idea/.gitignore new file mode 100644 index 0000000..a16c839 --- /dev/null +++ b/api/.idea/.idea.WhatApi/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/projectSettingsUpdater.xml +/contentModel.xml +/.idea.WhatApi.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml b/api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml new file mode 100644 index 0000000..bc9adff --- /dev/null +++ b/api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + postgis + + + \ No newline at end of file diff --git a/api/.idea/.idea.WhatApi/.idea/encodings.xml b/api/.idea/.idea.WhatApi/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/api/.idea/.idea.WhatApi/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/api/.idea/.idea.WhatApi/.idea/indexLayout.xml b/api/.idea/.idea.WhatApi/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/api/.idea/.idea.WhatApi/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml b/api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..5bb18c4 --- /dev/null +++ b/api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/WhatApi.sln b/api/WhatApi.sln new file mode 100644 index 0000000..b8ebda5 --- /dev/null +++ b/api/WhatApi.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhatApi", "WhatApi\WhatApi.csproj", "{09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/api/WhatApi/Constants.cs b/api/WhatApi/Constants.cs new file mode 100644 index 0000000..01385c1 --- /dev/null +++ b/api/WhatApi/Constants.cs @@ -0,0 +1,6 @@ +namespace WhatApi; + +public static class Constants +{ + public const int Wgs84SpatialReferenceId = 4326; +} \ No newline at end of file diff --git a/api/WhatApi/Database.cs b/api/WhatApi/Database.cs new file mode 100644 index 0000000..39de79a --- /dev/null +++ b/api/WhatApi/Database.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; + +namespace WhatApi; + +public class Database(DbContextOptions options) : DbContext(options) +{ + public DbSet Content { get; set; } + public DbSet Places { get; set; } + protected override void OnModelCreating(ModelBuilder b) { + b.HasPostgresExtension("postgis"); + b.Entity(e => { + e.Property(x => x.Location).HasColumnType($"geometry(point,{Constants.Wgs84SpatialReferenceId})"); + e.HasIndex(x => x.Location).HasMethod("gist"); + e.ToTable("Place"); + }); + b.Entity(); + base.OnModelCreating(b); + } +} \ No newline at end of file diff --git a/api/WhatApi/Dockerfile b/api/WhatApi/Dockerfile new file mode 100644 index 0000000..d559b98 --- /dev/null +++ b/api/WhatApi/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["WhatApi/WhatApi.csproj", "WhatApi/"] +RUN dotnet restore "WhatApi/WhatApi.csproj" +COPY . . +WORKDIR "/src/WhatApi" +RUN dotnet build "./WhatApi.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./WhatApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "WhatApi.dll"] diff --git a/api/WhatApi/Endpoints/BaseEndpoint.cs b/api/WhatApi/Endpoints/BaseEndpoint.cs new file mode 100644 index 0000000..4d8f9ad --- /dev/null +++ b/api/WhatApi/Endpoints/BaseEndpoint.cs @@ -0,0 +1,12 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc; + +namespace WhatApi.Endpoints; + +[ApiController] +public class BaseEndpoint : ControllerBase +{ + protected IPAddress GetIp() => Request.Headers.TryGetValue("X-Forwarded-For", out var ip) + ? IPAddress.Parse(ip.ToString()) + : HttpContext.Connection.RemoteIpAddress!; +} \ No newline at end of file diff --git a/api/WhatApi/Endpoints/DownloadContentEndpoint.cs b/api/WhatApi/Endpoints/DownloadContentEndpoint.cs new file mode 100644 index 0000000..dbe6bff --- /dev/null +++ b/api/WhatApi/Endpoints/DownloadContentEndpoint.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WhatApi.Endpoints; + +public class DownloadContentEndpoint : BaseEndpoint +{ + [HttpGet("~/{id:guid}")] + public async Task HandleAsync(Guid id, CancellationToken ct = default) { + try { + var path = Path.Combine(Directory.GetCurrentDirectory(), "files", id.ToString()); + await using var file = new FileStream(path, FileMode.Open); + if (!file.CanRead) return NotFound(); + return File(file, "application/octet-stream", id.ToString()); + } catch (Exception e) { + if (e is not FileNotFoundException) Console.WriteLine(e); + return NotFound(); + } + } +} \ No newline at end of file diff --git a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs new file mode 100644 index 0000000..5630229 --- /dev/null +++ b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NetTopologySuite; +using NetTopologySuite.Features; +using NetTopologySuite.Geometries; +using WhatApi.Tables; + +namespace WhatApi.Endpoints; + +public class GetPlacesEndpoint(Database db) : BaseEndpoint +{ + [HttpGet("~/places")] + public async Task HandleAsync(string w, string s, string e, string n, CancellationToken ct = default) { + var north = double.Parse(n); + var east = double.Parse(e); + var south = double.Parse(s); + var west = double.Parse(w); + + IQueryable resultingQuery; + + if (west > east) { + resultingQuery = db.Places + .FromSqlInterpolated($""" + SELECT * FROM "Place" + WHERE ST_Intersects( + "Location", + ST_MakeEnvelope({west}, {south}, 180, {north}, {Constants.Wgs84SpatialReferenceId}) || ST_MakeEnvelope(-180, {south}, {east}, {north}, {Constants.Wgs84SpatialReferenceId}) + ) + """); + } else { + resultingQuery = db.Places + .FromSqlInterpolated($""" + SELECT * FROM "Place" + WHERE ST_Intersects( + "Location", + ST_MakeEnvelope({west}, {south}, {east}, {north}, {Constants.Wgs84SpatialReferenceId}) + ) + """); + } + + var gf = NtsGeometryServices.Instance.CreateGeometryFactory(srid: Constants.Wgs84SpatialReferenceId); + var fc = new FeatureCollection(); + + await foreach (var p in resultingQuery.AsAsyncEnumerable().WithCancellation(ct)) { + var point = gf.CreatePoint(new Coordinate(p.Location.X, p.Location.Y)); + fc.Add(new Feature(point, new AttributesTable { + { + "id", p.Id + }, { + "cid", p.ContentId + } + })); + } + + return Ok(fc); + } +} \ No newline at end of file diff --git a/api/WhatApi/Endpoints/UploadContentEndpoint.cs b/api/WhatApi/Endpoints/UploadContentEndpoint.cs new file mode 100644 index 0000000..82fb71b --- /dev/null +++ b/api/WhatApi/Endpoints/UploadContentEndpoint.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using NetTopologySuite; +using NetTopologySuite.Geometries; + +namespace WhatApi.Endpoints; + +public class UploadContentEndpoint(Database db) : BaseEndpoint +{ + public record UploadContent(IFormFile File, string LatLong); + + [HttpPost("~/upload")] + public async Task HandleAsync([FromForm] UploadContent request, CancellationToken ct = default) { + if (string.IsNullOrWhiteSpace(Request.GetMultipartBoundary())) { + return StatusCode(415, "Unsupported Media Type"); + } + + var blobId = Guid.NewGuid(); + var contentId = Guid.NewGuid(); + + var latitude = request.LatLong.Split(',')[0]; + var longitude = request.LatLong.Split(',')[1]; + + var gf = NtsGeometryServices.Instance.CreateGeometryFactory(srid: Constants.Wgs84SpatialReferenceId); + var point = gf.CreatePoint(new Coordinate(double.Parse(longitude), double.Parse(latitude))); + + var place = new Tables.Place() { + ContentId = contentId, + Location = point + }; + + var content = new Tables.Content() { + Id = contentId, + Mime = request.File.ContentType, + Created = DateTime.UtcNow, + BlobId = blobId, + Ip = GetIp() + }; + + var path = Path.Combine(Directory.GetCurrentDirectory(), "files", blobId.ToString()); + + await using var writer = new FileStream(path, FileMode.CreateNew); + await request.File.CopyToAsync(writer, ct); + await db.Content.AddAsync(content, ct); + await db.Places.AddAsync(place, ct); + await db.SaveChangesAsync(ct); + return Ok(contentId); + } +} \ No newline at end of file diff --git a/api/WhatApi/Middleware/AuthenticationMiddleware.cs b/api/WhatApi/Middleware/AuthenticationMiddleware.cs new file mode 100644 index 0000000..8cdce53 --- /dev/null +++ b/api/WhatApi/Middleware/AuthenticationMiddleware.cs @@ -0,0 +1,8 @@ +namespace WhatApi.Middleware; + +public class AuthenticationMiddleware(RequestDelegate next,Database db) +{ + public async Task InvokeAsync(HttpContext context) { + await next(context); + } +} \ No newline at end of file diff --git a/api/WhatApi/Middleware/AuthorizationMiddleware.cs b/api/WhatApi/Middleware/AuthorizationMiddleware.cs new file mode 100644 index 0000000..26b3f4a --- /dev/null +++ b/api/WhatApi/Middleware/AuthorizationMiddleware.cs @@ -0,0 +1,8 @@ +namespace WhatApi.Middleware; + +public class AuthorizationMiddleware(RequestDelegate next, Database db) +{ + public async Task InvokeAsync(HttpContext context) { + await next(context); + } +} \ No newline at end of file diff --git a/api/WhatApi/Middleware/UserLastSeenMiddleware.cs b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs new file mode 100644 index 0000000..ef1b685 --- /dev/null +++ b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs @@ -0,0 +1,8 @@ +namespace WhatApi.Middleware; + +public class UserLastSeenMiddleware(RequestDelegate next, Database db) +{ + public async Task InvokeAsync(HttpContext context) { + await next(context); + } +} \ No newline at end of file diff --git a/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs b/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs new file mode 100644 index 0000000..5ddcc9f --- /dev/null +++ b/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs @@ -0,0 +1,93 @@ +// +using System; +using System.Net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WhatApi; + +#nullable disable + +namespace WhatApi.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20251013213511_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("WhatApi.Tables.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlobId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("inet"); + + b.Property("Mime") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Content"); + }); + + modelBuilder.Entity("WhatApi.Tables.Place", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentId") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("geometry(point,4326)"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex("Location"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist"); + + b.ToTable("Place", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.Place", b => + { + b.HasOne("WhatApi.Tables.Content", "Content") + .WithMany() + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/WhatApi/Migrations/20251013213511_Initial.cs b/api/WhatApi/Migrations/20251013213511_Initial.cs new file mode 100644 index 0000000..1fa8bbf --- /dev/null +++ b/api/WhatApi/Migrations/20251013213511_Initial.cs @@ -0,0 +1,75 @@ +using System; +using System.Net; +using Microsoft.EntityFrameworkCore.Migrations; +using NetTopologySuite.Geometries; + +#nullable disable + +namespace WhatApi.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:postgis", ",,"); + + migrationBuilder.CreateTable( + name: "Content", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Mime = table.Column(type: "text", nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false), + BlobId = table.Column(type: "uuid", nullable: false), + Ip = table.Column(type: "inet", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Content", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Place", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ContentId = table.Column(type: "uuid", nullable: false), + Location = table.Column(type: "geometry(point,4326)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Place", x => x.Id); + table.ForeignKey( + name: "FK_Place_Content_ContentId", + column: x => x.ContentId, + principalTable: "Content", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Place_ContentId", + table: "Place", + column: "ContentId"); + + migrationBuilder.CreateIndex( + name: "IX_Place_Location", + table: "Place", + column: "Location") + .Annotation("Npgsql:IndexMethod", "gist"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Place"); + + migrationBuilder.DropTable( + name: "Content"); + } + } +} diff --git a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs new file mode 100644 index 0000000..f1e5fcb --- /dev/null +++ b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs @@ -0,0 +1,90 @@ +// +using System; +using System.Net; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using WhatApi; + +#nullable disable + +namespace WhatApi.Migrations +{ + [DbContext(typeof(Database))] + partial class DatabaseModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("WhatApi.Tables.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlobId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("inet"); + + b.Property("Mime") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Content"); + }); + + modelBuilder.Entity("WhatApi.Tables.Place", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentId") + .HasColumnType("uuid"); + + b.Property("Location") + .IsRequired() + .HasColumnType("geometry(point,4326)"); + + b.HasKey("Id"); + + b.HasIndex("ContentId"); + + b.HasIndex("Location"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist"); + + b.ToTable("Place", (string)null); + }); + + modelBuilder.Entity("WhatApi.Tables.Place", b => + { + b.HasOne("WhatApi.Tables.Content", "Content") + .WithMany() + .HasForeignKey("ContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Content"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/api/WhatApi/Pages/Map.cshtml b/api/WhatApi/Pages/Map.cshtml new file mode 100644 index 0000000..31adf86 --- /dev/null +++ b/api/WhatApi/Pages/Map.cshtml @@ -0,0 +1,92 @@ +@page +@model WhatApi.Pages.Map + +@{ + Layout = null; +} + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/api/WhatApi/Pages/Map.cshtml.cs b/api/WhatApi/Pages/Map.cshtml.cs new file mode 100644 index 0000000..786180b --- /dev/null +++ b/api/WhatApi/Pages/Map.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace WhatApi.Pages; + +public class Map : PageModel +{ + public void OnGet() { + + } +} \ No newline at end of file diff --git a/api/WhatApi/Pages/Upload.cshtml b/api/WhatApi/Pages/Upload.cshtml new file mode 100644 index 0000000..4c87b11 --- /dev/null +++ b/api/WhatApi/Pages/Upload.cshtml @@ -0,0 +1,82 @@ +@page +@model WhatApi.Pages.Upload + +@{ + Layout = null; +} + + + + + + + + + + +
+
+ + + +
+
+
+

+
+
+
+
\ No newline at end of file
diff --git a/api/WhatApi/Pages/Upload.cshtml.cs b/api/WhatApi/Pages/Upload.cshtml.cs
new file mode 100644
index 0000000..2fe362e
--- /dev/null
+++ b/api/WhatApi/Pages/Upload.cshtml.cs
@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace WhatApi.Pages;
+
+public class Upload : PageModel
+{
+    public void OnGet() {
+
+    }
+}
\ No newline at end of file
diff --git a/api/WhatApi/Program.cs b/api/WhatApi/Program.cs
new file mode 100644
index 0000000..80d06e8
--- /dev/null
+++ b/api/WhatApi/Program.cs
@@ -0,0 +1,45 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.EntityFrameworkCore;
+using NetTopologySuite.IO.Converters;
+
+namespace WhatApi;
+
+public partial class Program
+{
+    public static int Main(string[] args) {
+        var builder = WebApplication.CreateBuilder(args);
+
+        builder.Services.AddDbContextPool(b => {
+            b.EnableSensitiveDataLogging();
+            b.UseNpgsql(builder.Configuration.GetConnectionString("Master"), o => o.UseNetTopologySuite());
+        });
+
+        builder.Services.AddRazorPages();
+        builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
+        builder.Services.AddControllers()
+            .AddJsonOptions(o => {
+                o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+                o.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
+                o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+                o.JsonSerializerOptions.Converters.Add(new GeoJsonConverterFactory());
+            });
+
+        var app = builder.Build();
+
+#if DEBUG
+        using var scope = app.Services.CreateScope();
+        var db = scope.ServiceProvider.GetRequiredService();
+        Seed(db);
+#endif
+
+        app.UseRouting();
+        app.UseForwardedHeaders();
+        app.UseCors();
+        app.MapStaticAssets();
+        app.MapRazorPages();
+        app.MapControllers();
+        app.Run();
+        return 0;
+    }
+}
\ No newline at end of file
diff --git a/api/WhatApi/Properties/launchSettings.json b/api/WhatApi/Properties/launchSettings.json
new file mode 100644
index 0000000..581ee7b
--- /dev/null
+++ b/api/WhatApi/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "https://json.schemastore.org/launchsettings.json",
+  "profiles": {
+    "http": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "launchBrowser": false,
+      "applicationUrl": "http://localhost:5281",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}
diff --git a/api/WhatApi/Seed.cs b/api/WhatApi/Seed.cs
new file mode 100644
index 0000000..c3127cc
--- /dev/null
+++ b/api/WhatApi/Seed.cs
@@ -0,0 +1,24 @@
+using Bogus;
+using Bogus.Locations;
+using NetTopologySuite.Geometries;
+using WhatApi.Tables;
+
+namespace WhatApi;
+
+public partial class Program
+{
+    private static void Seed(Database db) {
+        if (db.Places.Any() || true) return;
+        var places = new List();
+        var location = new Faker().Location();
+        for (var i = 0; i < 1000; i++) {
+            var point = location.AreaCircle(59.91838, 10.73861, 30000);
+            places.Add(new Place() {
+                Location = new Point(new Coordinate(point.Longitude, point.Latitude)),
+                ContentId = new Guid("1337710a-8cdb-4d50-815f-772c0e9f1482")
+            });
+        }
+        db.Places.AddRange(places);
+        db.SaveChanges();
+    }
+}
\ No newline at end of file
diff --git a/api/WhatApi/Tables/Content.cs b/api/WhatApi/Tables/Content.cs
new file mode 100644
index 0000000..79f2579
--- /dev/null
+++ b/api/WhatApi/Tables/Content.cs
@@ -0,0 +1,12 @@
+using System.Net;
+
+namespace WhatApi.Tables;
+
+public class Content
+{
+    public Guid Id { get; set; }
+    public string Mime { get; set; }
+    public DateTime Created { get; set; }
+    public Guid BlobId { get; set; }
+    public IPAddress Ip { get; set; }
+}
\ No newline at end of file
diff --git a/api/WhatApi/Tables/Place.cs b/api/WhatApi/Tables/Place.cs
new file mode 100644
index 0000000..ff95c96
--- /dev/null
+++ b/api/WhatApi/Tables/Place.cs
@@ -0,0 +1,11 @@
+using NetTopologySuite.Geometries;
+
+namespace WhatApi.Tables;
+
+public class Place
+{
+    public Guid Id { get; set; }
+    public Guid ContentId { get; set; }
+    public Content Content { get; set; }
+    public required Point Location { get; set; }
+}
\ No newline at end of file
diff --git a/api/WhatApi/Tables/Session.cs b/api/WhatApi/Tables/Session.cs
new file mode 100644
index 0000000..a0affa8
--- /dev/null
+++ b/api/WhatApi/Tables/Session.cs
@@ -0,0 +1,10 @@
+namespace WhatApi.Tables;
+
+public class Session
+{
+    public Guid Id { get; set; }
+    public string Token { get; set; }
+    public DateTime Created { get; set; }
+    public DateTime? Expires { get; set; }
+    public User User { get; set; }
+}
\ No newline at end of file
diff --git a/api/WhatApi/Tables/User.cs b/api/WhatApi/Tables/User.cs
new file mode 100644
index 0000000..0ebe9b6
--- /dev/null
+++ b/api/WhatApi/Tables/User.cs
@@ -0,0 +1,12 @@
+namespace WhatApi.Tables;
+
+public class User
+{
+    public Guid Id { get; set; }
+    public string Name { get; set; }
+    public string Email { get; set; }
+    public string Password { get; set; }
+    public DateTime Created { get; set; }
+    public DateTime? LastSeen { get; set; }
+    public IEnumerable Places { get; set; }
+}
\ No newline at end of file
diff --git a/api/WhatApi/WhatApi.csproj b/api/WhatApi/WhatApi.csproj
new file mode 100644
index 0000000..84dac71
--- /dev/null
+++ b/api/WhatApi/WhatApi.csproj
@@ -0,0 +1,34 @@
+
+
+    
+        net9.0
+        enable
+        enable
+        Linux
+      5506c159-f534-4090-b80b-2703e1eb7f6c
+  
+
+    
+      
+        .dockerignore
+      
+    
+
+    
+      
+      
+      
+      
+        all
+        runtime; build; native; contentfiles; analyzers; buildtransitive
+      
+      
+      
+      
+    
+
+    
+      
+    
+
+
diff --git a/api/WhatApi/appsettings.Development.json b/api/WhatApi/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/api/WhatApi/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  }
+}
diff --git a/api/WhatApi/appsettings.json b/api/WhatApi/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/api/WhatApi/appsettings.json
@@ -0,0 +1,9 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "AllowedHosts": "*"
+}
diff --git a/api/WhatApi/wwwroot/pin.png b/api/WhatApi/wwwroot/pin.png
new file mode 100644
index 0000000..3603031
Binary files /dev/null and b/api/WhatApi/wwwroot/pin.png differ
diff --git a/ios/H--appen-Info.plist b/ios/H--appen-Info.plist
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/ios/H--appen-Info.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git "a/ios/H\303\246-appen.xcodeproj/project.pbxproj" "b/ios/H\303\246-appen.xcodeproj/project.pbxproj"
new file mode 100644
index 0000000..d237a7b
--- /dev/null
+++ "b/ios/H\303\246-appen.xcodeproj/project.pbxproj"
@@ -0,0 +1,381 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 77;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1B9B791B2E7A06C300D5AF05 /* GISTools in Frameworks */ = {isa = PBXBuildFile; productRef = 1B9B791A2E7A06C300D5AF05 /* GISTools */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		1B9B79032E79DFB800D5AF05 /* Hæ-appen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hæ-appen.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+		1B9B79052E79DFB800D5AF05 /* Hæ-appen */ = {
+			isa = PBXFileSystemSynchronizedRootGroup;
+			path = "Hæ-appen";
+			sourceTree = "";
+		};
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		1B9B79002E79DFB800D5AF05 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1B9B791B2E7A06C300D5AF05 /* GISTools in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		1B9B78FA2E79DFB800D5AF05 = {
+			isa = PBXGroup;
+			children = (
+				1B9B79052E79DFB800D5AF05 /* Hæ-appen */,
+				1B9B79042E79DFB800D5AF05 /* Products */,
+			);
+			sourceTree = "";
+		};
+		1B9B79042E79DFB800D5AF05 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				1B9B79032E79DFB800D5AF05 /* Hæ-appen.app */,
+			);
+			name = Products;
+			sourceTree = "";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		1B9B79022E79DFB800D5AF05 /* Hæ-appen */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1B9B790E2E79DFBA00D5AF05 /* Build configuration list for PBXNativeTarget "Hæ-appen" */;
+			buildPhases = (
+				1B9B78FF2E79DFB800D5AF05 /* Sources */,
+				1B9B79002E79DFB800D5AF05 /* Frameworks */,
+				1B9B79012E79DFB800D5AF05 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			fileSystemSynchronizedGroups = (
+				1B9B79052E79DFB800D5AF05 /* Hæ-appen */,
+			);
+			name = "Hæ-appen";
+			packageProductDependencies = (
+				1B9B791A2E7A06C300D5AF05 /* GISTools */,
+			);
+			productName = "Hæ-appen";
+			productReference = 1B9B79032E79DFB800D5AF05 /* Hæ-appen.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		1B9B78FB2E79DFB800D5AF05 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 2600;
+				LastUpgradeCheck = 2600;
+				TargetAttributes = {
+					1B9B79022E79DFB800D5AF05 = {
+						CreatedOnToolsVersion = 26.0;
+					};
+				};
+			};
+			buildConfigurationList = 1B9B78FE2E79DFB800D5AF05 /* Build configuration list for PBXProject "Hæ-appen" */;
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 1B9B78FA2E79DFB800D5AF05;
+			minimizedProjectReferenceProxies = 1;
+			packageReferences = (
+				1B9B79192E7A06C300D5AF05 /* XCRemoteSwiftPackageReference "gis-tools" */,
+			);
+			preferredProjectObjectVersion = 77;
+			productRefGroup = 1B9B79042E79DFB800D5AF05 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				1B9B79022E79DFB800D5AF05 /* Hæ-appen */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		1B9B79012E79DFB800D5AF05 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		1B9B78FF2E79DFB800D5AF05 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		1B9B790C2E79DFBA00D5AF05 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		1B9B790D2E79DFBA00D5AF05 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		1B9B790F2E79DFBA00D5AF05 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = ZH95BL9NUK;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = "H--appen-Info.plist";
+				INFOPLIST_KEY_CFBundleDisplayName = "Hæ";
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
+				INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "";
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+				IPHONEOS_DEPLOYMENT_TARGET = 17.6;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = ivarivarivar.Haeappen;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRING_CATALOG_GENERATE_SYMBOLS = YES;
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SWIFT_APPROACHABLE_CONCURRENCY = YES;
+				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 1;
+			};
+			name = Debug;
+		};
+		1B9B79102E79DFBA00D5AF05 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = ZH95BL9NUK;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = "H--appen-Info.plist";
+				INFOPLIST_KEY_CFBundleDisplayName = "Hæ";
+				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
+				INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "";
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+				IPHONEOS_DEPLOYMENT_TARGET = 17.6;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = ivarivarivar.Haeappen;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRING_CATALOG_GENERATE_SYMBOLS = YES;
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SWIFT_APPROACHABLE_CONCURRENCY = YES;
+				SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 1;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		1B9B78FE2E79DFB800D5AF05 /* Build configuration list for PBXProject "Hæ-appen" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1B9B790C2E79DFBA00D5AF05 /* Debug */,
+				1B9B790D2E79DFBA00D5AF05 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		1B9B790E2E79DFBA00D5AF05 /* Build configuration list for PBXNativeTarget "Hæ-appen" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1B9B790F2E79DFBA00D5AF05 /* Debug */,
+				1B9B79102E79DFBA00D5AF05 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+		1B9B79192E7A06C300D5AF05 /* XCRemoteSwiftPackageReference "gis-tools" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/Outdooractive/gis-tools.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 1.3.0;
+			};
+		};
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		1B9B791A2E7A06C300D5AF05 /* GISTools */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 1B9B79192E7A06C300D5AF05 /* XCRemoteSwiftPackageReference "gis-tools" */;
+			productName = GISTools;
+		};
+/* End XCSwiftPackageProductDependency section */
+	};
+	rootObject = 1B9B78FB2E79DFB800D5AF05 /* Project object */;
+}
diff --git "a/ios/H\303\246-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata" "b/ios/H\303\246-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata"
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ "b/ios/H\303\246-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata"
@@ -0,0 +1,7 @@
+
+
+   
+   
+
diff --git "a/ios/H\303\246-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" "b/ios/H\303\246-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
new file mode 100644
index 0000000..36b946c
--- /dev/null
+++ "b/ios/H\303\246-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
@@ -0,0 +1,15 @@
+{
+  "originHash" : "a1cca039aa1704ebabd161056262205e26a4daf8a42ebb77bc98b0b620ee8fb4",
+  "pins" : [
+    {
+      "identity" : "gis-tools",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Outdooractive/gis-tools.git",
+      "state" : {
+        "revision" : "f7a4d2aa2acf771b79a91150f359bcd84de9ceac",
+        "version" : "1.13.2"
+      }
+    }
+  ],
+  "version" : 3
+}
diff --git "a/ios/H\303\246-appen/Assets.xcassets/AccentColor.colorset/Contents.json" "b/ios/H\303\246-appen/Assets.xcassets/AccentColor.colorset/Contents.json"
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ "b/ios/H\303\246-appen/Assets.xcassets/AccentColor.colorset/Contents.json"
@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git "a/ios/H\303\246-appen/Assets.xcassets/AppIcon.appiconset/Contents.json" "b/ios/H\303\246-appen/Assets.xcassets/AppIcon.appiconset/Contents.json"
new file mode 100644
index 0000000..2305880
--- /dev/null
+++ "b/ios/H\303\246-appen/Assets.xcassets/AppIcon.appiconset/Contents.json"
@@ -0,0 +1,35 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "tinted"
+        }
+      ],
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git "a/ios/H\303\246-appen/Assets.xcassets/Contents.json" "b/ios/H\303\246-appen/Assets.xcassets/Contents.json"
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ "b/ios/H\303\246-appen/Assets.xcassets/Contents.json"
@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git "a/ios/H\303\246-appen/ClusterHit.swift" "b/ios/H\303\246-appen/ClusterHit.swift"
new file mode 100644
index 0000000..ebab096
--- /dev/null
+++ "b/ios/H\303\246-appen/ClusterHit.swift"
@@ -0,0 +1,18 @@
+//
+//  ClusterHit.swift
+//  Hæ-appen
+//
+//  Created by Ivar Løvlie on 16/09/2025.
+//
+import GISTools
+import WebKit
+
+struct ClusterHit : Decodable { }
+
+final class ClusterProvider {
+    func load(west:Int, south: Int, north: Int, east: Int) async throws -> [ClusterHit] {
+        let url = URL(string: "http://http://localhost:5281/places?w=\(west)&e=\(east)&s=\(south)&n=\(north)")!
+        let (data, _) = try await URLSession.shared.data(from: url)
+        return try JSONDecoder().decode([ClusterHit].self, from: data)
+    }
+}
diff --git "a/ios/H\303\246-appen/HaeappenApp.swift" "b/ios/H\303\246-appen/HaeappenApp.swift"
new file mode 100644
index 0000000..e8aac4d
--- /dev/null
+++ "b/ios/H\303\246-appen/HaeappenApp.swift"
@@ -0,0 +1,17 @@
+//
+//  H__appenApp.swift
+//  Hæ-appen
+//
+//  Created by Ivar Løvlie on 16/09/2025.
+//
+
+import SwiftUI
+
+@main
+struct HaeappenApp: App {
+    var body: some Scene {
+        WindowGroup {
+            TabBarView()
+        }
+    }
+}
diff --git "a/ios/H\303\246-appen/LocationAuthorizer.swift" "b/ios/H\303\246-appen/LocationAuthorizer.swift"
new file mode 100644
index 0000000..fb6f3c8
--- /dev/null
+++ "b/ios/H\303\246-appen/LocationAuthorizer.swift"
@@ -0,0 +1,20 @@
+//
+//  LocationAuthorizer.swift
+//  Hæ-appen
+//
+//  Created by Ivar Løvlie on 16/10/2025.
+//
+
+import SwiftUI
+import MapKit
+import CoreLocation
+
+final class LocationAuthorizer: NSObject, CLLocationManagerDelegate {
+    static let shared = LocationAuthorizer()
+    private let manager = CLLocationManager()
+
+    func requestWhenInUse() {
+        manager.delegate = self
+        manager.requestWhenInUseAuthorization()
+    }
+}
diff --git "a/ios/H\303\246-appen/MapContentView.swift" "b/ios/H\303\246-appen/MapContentView.swift"
new file mode 100644
index 0000000..4d35dfe
--- /dev/null
+++ "b/ios/H\303\246-appen/MapContentView.swift"
@@ -0,0 +1,45 @@
+//
+//  ContentView.swift
+//  Hæ-appen
+//
+//  Created by Ivar Løvlie on 16/09/2025.
+//
+
+import SwiftUI
+import MapKit
+
+
+struct MapContentView: View {
+    @Namespace var mapScope
+    @State private var position: MapCameraPosition = .automatic
+    var annotationStringKey: LocalizedStringKey = "Annotation"
+    var annotationCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D.init()
+    let symbolSet: [String] = ["cloud.bolt.rain.fill", "sun.rain.fill", "moon.stars.fill", "moon.fill"]
+    
+    var body: some View {
+        if #available(iOS 26, *) {
+            VStack {
+                Map(initialPosition: .userLocation(fallback: position), scope: mapScope)
+            }.safeAreaInset(edge: .trailing) {
+                GlassEffectContainer(spacing: 10.0) {
+                    HStack(spacing: 20.0) {
+                        ForEach(symbolSet.indices, id: \.self) { item in
+                            Image(systemName: symbolSet[item])
+                                .frame(width: 80.0, height: 80.0)
+                                .font(.system(size: 36))
+                                .glassEffect()
+                                .glassEffectUnion(id: item < 2 ? "1" : "2", namespace: mapScope)
+                        }
+                    }
+                }
+            }.mapScope(mapScope)
+                .task {
+                    LocationAuthorizer.shared.requestWhenInUse()
+                }
+        }
+    }
+}
+
+#Preview {
+    MapContentView()
+}
diff --git "a/ios/H\303\246-appen/PostAnnotationView.swift" "b/ios/H\303\246-appen/PostAnnotationView.swift"
new file mode 100644
index 0000000..024015b
--- /dev/null
+++ "b/ios/H\303\246-appen/PostAnnotationView.swift"
@@ -0,0 +1,9 @@
+//
+//  PostAnnotationView.swift
+//  Hæ-appen
+//
+//  Created by Ivar Løvlie on 18/10/2025.
+//
+
+import Foundation
+
diff --git "a/ios/H\303\246-appen/TabBarView.swift" "b/ios/H\303\246-appen/TabBarView.swift"
new file mode 100644
index 0000000..ed0a89c
--- /dev/null
+++ "b/ios/H\303\246-appen/TabBarView.swift"
@@ -0,0 +1,22 @@
+//
+//  TabBarView.swift
+//  Hæ-appen
+//
+//  Created by Ivar Løvlie on 19/10/2025.
+//
+import SwiftUI
+
+struct TabBarView : View {
+    var body: some View {
+        if #available(iOS 26, *) {
+            TabView {
+                Tab("Hææ", systemImage: "house") {
+                    MapContentView()
+                }
+            }
+            .tabViewBottomAccessory {
+                Image(systemName: "person.fill")
+            }
+        }
+    }
+}
-- 
cgit v1.3