Compare commits

..

17 Commits

Author SHA1 Message Date
Matteo Rosati
fe4f135b92 add cursor style 2026-02-11 22:16:55 +01:00
Matteo Rosati
f05e91c2ce add styles 2026-02-11 21:52:15 +01:00
Matteo Rosati
9587995354 simplify apps 2026-02-11 21:05:49 +01:00
Matteo Rosati
93dafb6a81 add django-stubs 2026-02-11 20:52:23 +01:00
Matteo Rosati
c116f8dcb1 remove unused stuff. add player energy 2026-02-11 18:03:59 +01:00
Matteo Rosati
24a26bd092 remove unused stuff. add player energy 2026-02-11 18:03:54 +01:00
Matteo Rosati
6acc708f46 add .env 2026-02-11 17:24:31 +01:00
Matteo Rosati
4c67b10f75 add buildings hover. add favicon 2026-02-11 17:09:06 +01:00
Matteo Rosati
335299c670 remove old CDN maplibre 2026-02-11 15:38:05 +01:00
Matteo Rosati
af95eea265 uniform syntax 2026-02-11 15:35:21 +01:00
Matteo Rosati
3884864d30 setup ts 2026-02-11 15:34:53 +01:00
Matteo Rosati
787bdede87 add minimal TS bundler 2026-02-11 15:15:27 +01:00
Matteo Rosati
a3ef6cf84c add js types 2026-02-11 12:45:04 +01:00
Matteo Rosati
01fd047568 display name uniqueness 2026-02-11 12:26:54 +01:00
Matteo Rosati
6cbaf5f6ee implement basic header 2026-02-11 12:22:07 +01:00
Matteo Rosati
ee60df0c33 code formatting 2026-02-11 12:05:58 +01:00
Matteo Rosati
6bce72e866 add register, login, logout views 2026-02-11 11:53:06 +01:00
43 changed files with 1147 additions and 231 deletions

2
.env.dist Normal file
View File

@@ -0,0 +1,2 @@
DEBUG=True
SECRET_KEY=1234567890

7
.gitignore vendored
View File

@@ -9,4 +9,11 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
# Environment files
.env
# Node modules (only for types definitions)
node_modules/
# Collected static files
staticfiles/ staticfiles/

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,5 +0,0 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

View File

@@ -1,22 +0,0 @@
# Generated by Django 6.0.2 on 2026-02-10 18:07
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Building',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('geojson_id', models.IntegerField()),
('health', models.FloatField()),
],
),
]

View File

@@ -1,6 +0,0 @@
from django.db import models
class Building(models.Model):
geojson_id = models.IntegerField()
health = models.FloatField()

View File

@@ -1,75 +0,0 @@
from django.contrib.auth import authenticate, get_user_model
from django.core.validators import validate_email
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework_simplejwt.tokens import RefreshToken
User = get_user_model()
class RegistrationSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(min_length=8, write_only=True)
def validate_email(self, value):
validate_email(value)
if User.objects.filter(email__iexact=value).exists():
raise serializers.ValidationError(_("Email is already registered."))
return value
def create(self, validated_data):
email = validated_data["email"].lower()
password = validated_data["password"]
user = User.objects.create_user(
username=email,
email=email,
password=password,
)
return user
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
def validate(self, attrs):
email = attrs.get("email", "").lower()
password = attrs.get("password")
if not email or not password:
raise serializers.ValidationError(_("Email and password are required."))
user = authenticate(
request=self.context.get("request"),
username=email,
password=password,
)
if user is None:
raise serializers.ValidationError(_("Invalid email or password."))
if not user.is_active:
raise serializers.ValidationError(_("User account is disabled."))
attrs["user"] = user
return attrs
def create(self, validated_data):
user = validated_data["user"]
refresh = RefreshToken.for_user(user)
return {
"refresh": str(refresh),
"access": str(refresh.access_token),
}
class LogoutSerializer(serializers.Serializer):
refresh = serializers.CharField()
def validate(self, attrs):
refresh = attrs.get("refresh")
if not refresh:
raise serializers.ValidationError(_("Refresh token is required."))
return attrs
def create(self, validated_data):
refresh = RefreshToken(validated_data["refresh"])
refresh.blacklist()
return {}

View File

@@ -1,9 +0,0 @@
from django.urls import path
from .views import LoginView, LogoutView, RegisterView
urlpatterns = [
path("auth/register", RegisterView.as_view(), name="auth-register"),
path("auth/login", LoginView.as_view(), name="auth-login"),
path("auth/logout", LogoutView.as_view(), name="auth-logout"),
]

View File

@@ -1,67 +0,0 @@
from rest_framework import permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import LoginSerializer, LogoutSerializer, RegistrationSerializer
class RegisterView(APIView):
permission_classes = [permissions.AllowAny]
throttle_scope = "auth"
def post(self, request):
serializer = RegistrationSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"errors": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
user = serializer.save()
login_serializer = LoginSerializer(
data={
"email": user.email,
"password": request.data.get("password"),
},
context={"request": request},
)
login_serializer.is_valid(raise_exception=True)
tokens = login_serializer.save()
return Response(
{
"user": {
"id": user.id,
"email": user.email,
},
"tokens": tokens,
},
status=status.HTTP_201_CREATED,
)
class LoginView(APIView):
permission_classes = [permissions.AllowAny]
throttle_scope = "auth"
def post(self, request):
serializer = LoginSerializer(data=request.data, context={"request": request})
if not serializer.is_valid():
return Response(
{"errors": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
tokens = serializer.save()
return Response({"tokens": tokens}, status=status.HTTP_200_OK)
class LogoutView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
serializer = LogoutSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"errors": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
serializer.save()
return Response({"detail": "Logged out."}, status=status.HTTP_200_OK)

31
build.js Normal file
View File

@@ -0,0 +1,31 @@
// build.js
import * as esbuild from "esbuild";
const isDev = process.argv.includes("--dev");
const config = {
entryPoints: ["frontend/static/frontend/ts/main.ts"], // il tuo entry point
bundle: true,
outdir: "frontend/static/frontend/dist/js",
format: "iife", // o 'iife' se serve per un tag <script> classico
target: "es2020",
sourcemap: isDev,
minify: true,
logLevel: "info",
};
if (isDev) {
const ctx = await esbuild.context(config);
await ctx.watch();
console.log("Watching...");
const shutdown = async () => {
await ctx.dispose();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
} else {
await esbuild.build(config);
}

128
bun.lock Normal file
View File

@@ -0,0 +1,128 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"dependencies": {
"@types/maplibre-gl": "^1.14.0",
"esbuild": "^0.27.3",
"maplibre-gl": "^5.18.0",
},
},
},
"packages": {
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
"@mapbox/geojson-rewind": ["@mapbox/geojson-rewind@0.5.2", "", { "dependencies": { "get-stream": "^6.0.1", "minimist": "^1.2.6" }, "bin": { "geojson-rewind": "geojson-rewind" } }, "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA=="],
"@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="],
"@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="],
"@mapbox/tiny-sdf": ["@mapbox/tiny-sdf@2.0.7", "", {}, "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug=="],
"@mapbox/unitbezier": ["@mapbox/unitbezier@0.0.1", "", {}, "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="],
"@mapbox/vector-tile": ["@mapbox/vector-tile@2.0.4", "", { "dependencies": { "@mapbox/point-geometry": "~1.1.0", "@types/geojson": "^7946.0.16", "pbf": "^4.0.1" } }, "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg=="],
"@mapbox/whoots-js": ["@mapbox/whoots-js@3.1.0", "", {}, "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="],
"@maplibre/geojson-vt": ["@maplibre/geojson-vt@5.0.4", "", {}, "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ=="],
"@maplibre/maplibre-gl-style-spec": ["@maplibre/maplibre-gl-style-spec@24.4.1", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", "rw": "^1.3.3", "tinyqueue": "^3.0.0" }, "bin": { "gl-style-migrate": "dist/gl-style-migrate.mjs", "gl-style-validate": "dist/gl-style-validate.mjs", "gl-style-format": "dist/gl-style-format.mjs" } }, "sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg=="],
"@maplibre/mlt": ["@maplibre/mlt@1.1.6", "", { "dependencies": { "@mapbox/point-geometry": "^1.1.0" } }, "sha512-rgtY3x65lrrfXycLf6/T22ZnjTg5WgIOsptOIoCaMZy4O4UAKTyZlYY0h6v8le721pTptF94U65yMDQkug+URw=="],
"@maplibre/vt-pbf": ["@maplibre/vt-pbf@4.2.1", "", { "dependencies": { "@mapbox/point-geometry": "^1.1.0", "@mapbox/vector-tile": "^2.0.4", "@maplibre/geojson-vt": "^5.0.4", "@types/geojson": "^7946.0.16", "@types/supercluster": "^7.1.3", "pbf": "^4.0.1", "supercluster": "^8.0.1" } }, "sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/maplibre-gl": ["@types/maplibre-gl@1.14.0", "", { "dependencies": { "maplibre-gl": "*" } }, "sha512-I6ibscT7UdL1oOqqCz9s1gjcolLaUPkHIIfMLusczTvlsMhjORyS6sE1g4V/NESAOL5KhNQX3/31LJH+OCGjkg=="],
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
"earcut": ["earcut@3.0.2", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="],
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="],
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
"maplibre-gl": ["maplibre-gl@5.18.0", "", { "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.0.7", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@maplibre/geojson-vt": "^5.0.4", "@maplibre/maplibre-gl-style-spec": "^24.4.1", "@maplibre/mlt": "^1.1.6", "@maplibre/vt-pbf": "^4.2.1", "@types/geojson": "^7946.0.16", "@types/supercluster": "^7.1.3", "earcut": "^3.0.2", "gl-matrix": "^3.4.4", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.1.0", "quickselect": "^3.0.0", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0" } }, "sha512-UtWxPBpHuFvEkM+5FVfcFG9ZKEWZQI6+PZkvLErr8Zs5ux+O7/KQ3JjSUvAfOlMeMgd/77qlHpOw0yHL7JU5cw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"murmurhash-js": ["murmurhash-js@1.0.0", "", {}, "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="],
"pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="],
"potpack": ["potpack@2.1.0", "", {}, "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="],
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
"quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="],
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
"tinyqueue": ["tinyqueue@3.0.0", "", {}, "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="],
}
}

Binary file not shown.

View File

@@ -10,34 +10,41 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/ https://docs.djangoproject.com/en/6.0/ref/settings/
""" """
import os
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env")
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-nows$x_3#k9x7j09f*o4xmx#p6zeb%ak+#ew2pc01yx2bm+hwq" SECRET_KEY = os.getenv(
"SECRET_KEY",
"django-insecure-nows$x_3#k9x7j09f*o4xmx#p6zeb%ak+#ew2pc01yx2bm+hwq",
)
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = os.getenv("DEBUG", "False").strip().lower() in {"1", "true", "yes", "on"}
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"api", "game",
"frontend", "frontend",
"rest_framework", "rest_framework",
"rest_framework_simplejwt", "rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist", "rest_framework_simplejwt.token_blacklist",
"django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",

View File

@@ -15,11 +15,8 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin
from django.urls import include, path from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("", include("frontend.urls")), path("", include("frontend.urls")),
path("admin/", admin.site.urls),
path("api/", include("api.urls")),
] ]

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,5 +0,0 @@
from django.apps import AppConfig
class FrontendConfig(AppConfig):
name = 'frontend'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,4 +1,284 @@
:root {
color-scheme: dark;
--map-ink: #041005;
--map-void: #020a03;
--map-forest: #0b1f0e;
--map-grid: #0f2f17;
--map-road: #1de85b;
--map-road-soft: #119a3b;
--map-node: #6dff9d;
--map-label: #7dffa6;
--map-glow: rgba(19, 240, 97, 0.35);
--map-glow-strong: rgba(19, 240, 97, 0.65);
--map-surface: rgba(6, 20, 9, 0.85);
--map-surface-strong: rgba(6, 24, 10, 0.96);
--map-border: rgba(29, 232, 91, 0.35);
--map-border-strong: rgba(29, 232, 91, 0.6);
--map-text: #b5ffd0;
--map-muted: #6bbf83;
--map-accent: #1de85b;
--map-accent-hot: #9bff5c;
}
body {
background: radial-gradient(
circle at top,
#0b1f0e 0%,
#041005 45%,
#020a03 100%
);
color: var(--map-text);
font-family:
"JetBrains Mono", "SFMono-Regular", "Menlo", "Monaco", "Consolas",
monospace;
}
strong {
font-weight: 800;
}
#map { #map {
width: 100%; width: 100%;
height: 100vh; height: 100vh;
background:
radial-gradient(
circle at 20% 10%,
rgba(12, 48, 20, 0.85),
rgba(4, 14, 7, 0.9)
),
linear-gradient(140deg, rgba(5, 24, 10, 0.95), rgba(2, 8, 4, 0.98));
}
.site-header {
position: fixed;
top: 1.5rem;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
width: min(1100px, calc(100% - 2.5rem));
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.85rem 1.5rem;
border-radius: 999px;
background: linear-gradient(
120deg,
rgba(7, 24, 11, 0.9),
rgba(6, 20, 9, 0.75)
);
backdrop-filter: blur(16px);
border: 1px solid var(--map-border);
box-shadow:
0 18px 40px rgba(0, 0, 0, 0.45),
0 0 24px var(--map-glow);
font-family:
"JetBrains Mono", "SFMono-Regular", "Menlo", "Monaco", "Consolas",
monospace;
color: var(--map-text);
}
.site-brand {
text-transform: uppercase;
letter-spacing: 0.25em;
font-size: 0.75rem;
color: inherit;
text-decoration: none;
font-weight: 600;
text-shadow: 0 0 12px var(--map-glow);
}
.site-actions {
display: flex;
align-items: center;
gap: 0.85rem;
font-size: 0.85rem;
}
.site-user {
color: var(--map-muted);
font-weight: 500;
}
.site-link {
text-decoration: none;
color: var(--map-text);
padding: 0.45rem 0.9rem;
border-radius: 999px;
border: 1px solid rgba(29, 232, 91, 0.25);
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
border-color 0.2s ease;
}
.site-link:hover {
transform: translateY(-1px);
border-color: var(--map-border-strong);
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.35),
0 0 16px var(--map-glow);
}
.site-link--accent {
border: none;
background: linear-gradient(130deg, #0fd64f, #9bff5c);
color: #041005;
font-weight: 600;
box-shadow: 0 0 18px rgba(15, 214, 79, 0.45);
}
@media (max-width: 720px) {
.site-header {
top: 1rem;
width: calc(100% - 1.5rem);
padding: 0.75rem 1rem;
border-radius: 24px;
flex-direction: column;
gap: 0.75rem;
}
.site-actions {
width: 100%;
justify-content: center;
flex-wrap: wrap;
gap: 0.65rem;
}
}
.auth-shell {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 4rem 1.5rem;
background: radial-gradient(
circle at top,
#0e2613,
#07180c 35%,
#041106 65%,
#020a03
);
color: var(--map-text);
font-family:
"JetBrains Mono", "SFMono-Regular", "Menlo", "Monaco", "Consolas",
monospace;
}
.auth-card {
width: min(420px, 100%);
background: var(--map-surface);
border-radius: 24px;
padding: 2.5rem;
box-shadow:
0 24px 60px rgba(0, 0, 0, 0.5),
0 0 22px var(--map-glow);
border: 1px solid var(--map-border);
}
.auth-header {
margin-bottom: 2rem;
}
.auth-kicker {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.7rem;
color: var(--map-muted);
margin-bottom: 0.75rem;
}
.auth-title {
font-size: 1.6rem;
margin-bottom: 0.5rem;
color: var(--map-text);
}
.auth-subtitle {
font-size: 0.95rem;
color: var(--map-muted);
}
.auth-messages {
margin-bottom: 1.5rem;
background: rgba(18, 55, 26, 0.75);
border: 1px solid rgba(110, 255, 157, 0.35);
border-radius: 16px;
padding: 0.75rem 1rem;
color: var(--map-accent-hot);
}
.auth-message {
font-size: 0.85rem;
margin: 0.25rem 0;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.auth-field {
display: flex;
flex-direction: column;
gap: 0.45rem;
font-size: 0.85rem;
color: var(--map-muted);
width: 100%;
}
.auth-field input {
padding: 0.7rem 0.9rem;
border-radius: 12px;
border: 1px solid rgba(109, 255, 157, 0.3);
font-size: 0.95rem;
font-family: inherit;
background: var(--map-surface-strong);
color: var(--map-text);
box-shadow: inset 0 0 0 1px rgba(3, 18, 7, 0.8);
width: 100%;
min-width: 0;
}
.auth-field input:focus {
outline: 2px solid rgba(29, 232, 91, 0.35);
border-color: var(--map-accent);
}
.auth-submit {
margin-top: 0.5rem;
padding: 0.8rem 1rem;
border-radius: 999px;
border: none;
background: linear-gradient(130deg, #0fd64f, #9bff5c);
color: #041005;
font-weight: 600;
cursor: pointer;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.auth-submit:hover {
transform: translateY(-1px);
box-shadow:
0 12px 25px rgba(15, 214, 79, 0.35),
0 0 18px var(--map-glow-strong);
}
.auth-footer {
margin-top: 1.5rem;
font-size: 0.85rem;
color: var(--map-muted);
}
.auth-footer a {
color: var(--map-text);
font-weight: 600;
text-decoration: none;
}
.auth-footer a:hover {
text-decoration: underline;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,13 @@
import { AddLayerObject } from "maplibre-gl";
export const HIGHLIGHT_LAYER = {
id: "buildings-highlight", // nome arbitrario del layer
type: "fill",
source: "buildings",
"source-layer": "building",
paint: {
"fill-color": "#41ff44",
"fill-opacity": 1,
},
filter: ["==", ["id"], ""],
} as AddLayerObject;

View File

@@ -0,0 +1,3 @@
export default function add(a: number, b: number): number {
return a + b;
}

View File

@@ -0,0 +1,41 @@
import { HIGHLIGHT_LAYER } from "./layers";
import { Map } from "maplibre-gl";
const map = new Map({
style:
"https://api.maptiler.com/maps/019be805-c88e-7c8b-9850-bc704d72e604/style.json?key=8nmgHEIZQiIgqQj3RZNa",
container: "map",
zoom: 17,
});
map.on("load", () => {
navigator.geolocation.getCurrentPosition((position) => {
map.panTo({
lat: position.coords.latitude,
lng: position.coords.longitude,
});
});
// Il layer per gli highlight
map.addLayer(HIGHLIGHT_LAYER);
map.on("mousemove", (ev) => {
const features = map.queryRenderedFeatures(ev.point, {
layers: ["buildings"], // questo e' il layer che c'e' nello style di maptiler
});
if (features && features.length > 0) {
const feature = features[0];
const hoveredId = feature.id;
if (hoveredId) {
map.setFilter("buildings-highlight", ["==", ["id"], hoveredId]);
map.getCanvas().style.cursor = "pointer";
}
} else {
map.setFilter("buildings-highlight", ["==", ["id"], ""]);
map.getCanvas().style.cursor = "";
}
});
});

View File

@@ -1,15 +1,19 @@
{% load static %} {% load static %}
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet"> <link
<link rel="stylesheet" href="{% static 'frontend/css/base.css' %}"> href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
<link rel="stylesheet" href="{% static 'frontend/css/reset.css' %}"> rel="stylesheet"
/>
<link rel="stylesheet" href="{% static 'frontend/css/base.css' %}" />
<link rel="stylesheet" href="{% static 'frontend/css/reset.css' %}" />
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/maplibre-gl/5.7.3/maplibre-gl.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/maplibre-gl/5.7.3/maplibre-gl.min.css"
@@ -17,18 +21,18 @@
crossorigin="anonymous" crossorigin="anonymous"
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />
<link rel="icon" type="image/x-icon" href="{% static 'frontend/images/favicon.ico' %}">
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script> <script
src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"
></script>
{% block js_top %}{% endblock %} {% block js_top %}{% endblock %}
</head> </head>
<body> <body>
<header>{% include "frontend/partials/header.html" %}</header>
{% block content %} {% endblock %} {% block content %} {% endblock %}
<script
src="https://cdnjs.cloudflare.com/ajax/libs/maplibre-gl/5.7.3/maplibre-gl.min.js"
integrity="sha512-Gx0xDElSrwjxjT9mjMg+OsoA0ekI8IkwuPurccWk5afkFBzXQHE0eQsQ7syopu9MJ0HD1EYGmVXjY8SPZt5FAg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
{% block js_bottom %}{% endblock %} {% block js_bottom %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -1,18 +1,13 @@
{% extends 'frontend/base.html' %} {% extends 'frontend/base.html' %}
{% block title %}Home{% endblock %} {% load static %}
{% block content%} {% block title %}ᑀ DroneWars ᑅ{% endblock %}
<div id="map"></div>
{% block content %}
<div id="map"></div>
{% endblock %} {% endblock %}
{% block js_bottom %} {% block js_bottom %}
<script> <script src="{% static 'frontend/dist/js/main.js' %}"></script>
var map = new maplibregl.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/019be805-c88e-7c8b-9850-bc704d72e604/style.json?key=8nmgHEIZQiIgqQj3RZNa',
center: [-74.5, 40],
zoom: 9
});
</script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends 'frontend/base.html' %}
{% block title %}Login{% endblock %}
{% block content %}
<main class="auth-shell">
<section class="auth-card">
<header class="auth-header">
<p class="auth-kicker">DroneWars</p>
<h1 class="auth-title">Welcome back</h1>
<p class="auth-subtitle">Log in to continue your flight plan.</p>
</header>
{% if messages %}
<div class="auth-messages">
{% for message in messages %}
<p class="auth-message">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" class="auth-form">
{% csrf_token %}
<label class="auth-field">
<span>Email</span>
<input type="email" name="email" autocomplete="email" required>
</label>
<label class="auth-field">
<span>Password</span>
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit" class="auth-submit">Log in</button>
</form>
<footer class="auth-footer">
<p>No account yet? <a href="{% url 'register' %}">Create one</a>.</p>
</footer>
</section>
</main>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'frontend/base.html' %}
{% block title %}Logout{% endblock %}
{% block content %}
<main class="auth-shell">
<section class="auth-card">
<header class="auth-header">
<p class="auth-kicker">DroneWars</p>
<h1 class="auth-title">Log out</h1>
<p class="auth-subtitle">Are you sure you want to end this session?</p>
</header>
<form method="post" class="auth-form">
{% csrf_token %}
<button type="submit" class="auth-submit">Confirm logout</button>
</form>
<footer class="auth-footer">
<p>Changed your mind? <a href="{% url 'home' %}">Return home</a>.</p>
</footer>
</section>
</main>
{% endblock %}

View File

@@ -0,0 +1,16 @@
<nav class="site-header">
<a class="site-brand" href="{% url 'home' %}">DroneWars</a>
<div class="site-actions">
{% if user.is_authenticated %}
<span class="site-user">
Welcome, <strong>{{ user.profile.display_name|default:user.get_username }}</strong>
</span>
<a class="site-link" href="{% url 'logout' %}">Logout</a>
{% else %}
<a class="site-link" href="{% url 'login' %}">Login</a>
<a class="site-link site-link--accent" href="{% url 'register' %}">
Register
</a>
{% endif %}
</div>
</nav>

View File

@@ -0,0 +1,45 @@
{% extends 'frontend/base.html' %}
{% block title %}Register{% endblock %}
{% block content %}
<main class="auth-shell">
<section class="auth-card">
<header class="auth-header">
<p class="auth-kicker">DroneWars</p>
<h1 class="auth-title">Create your pilot profile</h1>
<p class="auth-subtitle">Sign up with your email and a secure password.</p>
</header>
{% if messages %}
<div class="auth-messages">
{% for message in messages %}
<p class="auth-message">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" class="auth-form">
{% csrf_token %}
<label class="auth-field">
<span>Email</span>
<input type="email" name="email" autocomplete="email" required>
</label>
<label class="auth-field">
<span>Display name</span>
<input type="text" name="display_name" maxlength="150" autocomplete="nickname" required>
</label>
<label class="auth-field">
<span>Password</span>
<input type="password" name="password" autocomplete="new-password" required>
</label>
<label class="auth-field">
<span>Confirm password</span>
<input type="password" name="password_confirm" autocomplete="new-password" required>
</label>
<button type="submit" class="auth-submit">Create account</button>
</form>
<footer class="auth-footer">
<p>Already have an account? <a href="{% url 'login' %}">Log in</a>.</p>
</footer>
</section>
</main>
{% endblock %}

View File

@@ -2,7 +2,9 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("", views.home, name="home"), path("", views.home, name="home"),
path("auth/register/", views.register, name="register"),
path("auth/login/", views.login, name="login"),
path("auth/logout/", views.logout, name="logout"),
] ]

View File

@@ -1,5 +1,113 @@
from django.shortcuts import render from django.contrib import messages
from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.views.decorators.http import require_http_methods
from django_ratelimit.decorators import ratelimit
from game.models import UserProfile
def home(request): def home(request):
return render(request, "frontend/home.html") return render(request, "frontend/home.html")
@ratelimit(key="ip", rate="5/m", method="POST", block=False)
@require_http_methods(["GET", "POST"])
def register(request: HttpRequest) -> HttpResponse:
user = getattr(request, "user", None)
if user and user.is_authenticated:
return redirect("home")
if getattr(request, "limited", False):
messages.error(request, "Too many attempts. Try again in a minute.")
if request.method == "POST" and not getattr(request, "limited", False):
email = str(request.POST.get("email", "")).strip().lower()
display_name = str(request.POST.get("display_name", "")).strip()
password = request.POST.get("password") or ""
password_confirm = request.POST.get("password_confirm") or ""
errors: list[str] = []
if not email:
errors.append("Email is required.")
else:
try:
validate_email(email)
except ValidationError:
errors.append("Enter a valid email address.")
if not display_name:
errors.append("Display name is required.")
if not password:
errors.append("Password is required.")
elif password != password_confirm:
errors.append("Passwords do not match.")
if email and User.objects.filter(username=email).exists():
errors.append("An account with that email already exists.")
if (
display_name
and UserProfile.objects.filter( # type: ignore[attr-defined]
display_name=display_name
).exists()
):
errors.append("That display name is already taken.")
if errors:
for error in errors:
messages.error(request, error)
else:
user = User.objects.create_user(
username=email, email=email, password=password
)
UserProfile.objects.filter(user=user).update( # type: ignore[attr-defined]
display_name=display_name
)
auth_login(request, user)
return redirect("home")
return render(request, "frontend/register.html")
@ratelimit(key="ip", rate="10/m", method="POST", block=False)
@require_http_methods(["GET", "POST"])
def login(request: HttpRequest) -> HttpResponse:
user = getattr(request, "user", None)
if user and user.is_authenticated:
return redirect("home")
if getattr(request, "limited", False):
messages.error(request, "Too many attempts. Try again in a minute.")
if request.method == "POST" and not getattr(request, "limited", False):
email = str(request.POST.get("email", "")).strip().lower()
password = request.POST.get("password") or ""
if not email or not password:
messages.error(request, "Email and password are required.")
else:
user = authenticate(request, username=email, password=password)
if user is None:
messages.error(request, "Invalid email or password.")
else:
auth_login(request, user)
return redirect("home")
return render(request, "frontend/login.html")
@require_http_methods(["GET", "POST"])
def logout(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
auth_logout(request)
return redirect("home")
return render(request, "frontend/logout.html")

5
game/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class GameConfig(AppConfig):
name = 'game'

View File

@@ -0,0 +1,41 @@
# Generated by Django 6.0.2 on 2026-02-11 20:00
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Building',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('geojson_id', models.IntegerField()),
('health', models.FloatField()),
],
),
migrations.CreateModel(
name='PlayerInfo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('energy', models.PositiveIntegerField()),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='player_info', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('display_name', models.CharField(max_length=150, unique=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]

40
game/models.py Normal file
View File

@@ -0,0 +1,40 @@
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
class Building(models.Model):
geojson_id = models.IntegerField()
health = models.FloatField()
class PlayerInfo(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="player_info",
)
energy = models.PositiveIntegerField()
class UserProfile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
display_name = models.CharField(max_length=150, unique=True)
def __str__(self):
return f"{self.display_name} ({self.user.username})" # type: ignore[attr-defined]
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile_for_user(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(
user=instance,
display_name=instance.username,
)
PlayerInfo.objects.create(user=instance, energy=10_000)

3
game/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

11
package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"dependencies": {
"@types/maplibre-gl": "^1.14.0",
"esbuild": "^0.27.3",
"maplibre-gl": "^5.18.0"
},
"scripts": {
"dev": "bun run build.js --dev",
"build": "bun run build.js"
}
}

View File

@@ -6,9 +6,13 @@ readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"django>=6.0.2", "django>=6.0.2",
"django-ratelimit>=4.1.0",
"django-stubs>=5.2.9",
"djangorestframework>=3.16.1", "djangorestframework>=3.16.1",
"djangorestframework-simplejwt>=5.5.0", "djangorestframework-simplejwt>=5.5.0",
"ipython>=9.10.0",
"pydantic>=2.12.5", "pydantic>=2.12.5",
"python-dotenv>=1.2.1",
"uvicorn>=0.40.0", "uvicorn>=0.40.0",
"whitenoise>=6.11.0", "whitenoise>=6.11.0",
] ]

View File

@@ -1,15 +1,36 @@
annotated-types==0.7.0 annotated-types==0.7.0
asgiref==3.11.1 asgiref==3.11.1
asttokens==3.0.1
click==8.3.1 click==8.3.1
decorator==5.2.1
django==6.0.2 django==6.0.2
django-ratelimit==4.1.0
django-stubs==5.2.9
django-stubs-ext==5.2.9
djangorestframework==3.16.1 djangorestframework==3.16.1
djangorestframework-simplejwt==5.5.1 djangorestframework-simplejwt==5.5.1
executing==2.2.1
h11==0.16.0 h11==0.16.0
ipython==9.10.0
ipython-pygments-lexers==1.1.1
jedi==0.19.2
matplotlib-inline==0.2.1
parso==0.8.6
pexpect==4.9.0
prompt-toolkit==3.0.52
ptyprocess==0.7.0
pure-eval==0.2.3
pydantic==2.12.5 pydantic==2.12.5
pydantic-core==2.41.5 pydantic-core==2.41.5
pygments==2.19.2
pyjwt==2.11.0 pyjwt==2.11.0
python-dotenv==1.2.1
sqlparse==0.5.5 sqlparse==0.5.5
stack-data==0.6.3
traitlets==5.14.3
types-pyyaml==6.0.12.20250915
typing-extensions==4.15.0 typing-extensions==4.15.0
typing-inspection==0.4.2 typing-inspection==0.4.2
uvicorn==0.40.0 uvicorn==0.40.0
wcwidth==0.6.0
whitenoise==6.11.0 whitenoise==6.11.0

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"types": ["maplibre-gl"],
"lib": ["ES2020", "DOM"],
"noEmit": true
},
"include": ["frontend/static/frontend/ts/**/*.ts"]
}

239
uv.lock generated
View File

@@ -20,6 +20,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
] ]
[[package]]
name = "asttokens"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.1" version = "8.3.1"
@@ -41,6 +50,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "decorator"
version = "5.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
]
[[package]] [[package]]
name = "django" name = "django"
version = "6.0.2" version = "6.0.2"
@@ -55,6 +73,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" }, { url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" },
] ]
[[package]]
name = "django-ratelimit"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/8f/94038fe739b095aca3e4708ecc8a4e77f1fcfd87bed5d6baff43d4c80bc4/django-ratelimit-4.1.0.tar.gz", hash = "sha256:555943b283045b917ad59f196829530d63be2a39adb72788d985b90c81ba808b", size = 11551, upload-time = "2023-07-24T20:34:32.374Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/78/2c59b30cd8bc8068d02349acb6aeed5c4e05eb01cdf2107ccd76f2e81487/django_ratelimit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d047a31cf94d83ef1465d7543ca66c6fc16695559b5f8d814d1b51df15110b92", size = 11608, upload-time = "2023-07-24T20:34:31.362Z" },
]
[[package]]
name = "django-stubs"
version = "5.2.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "django-stubs-ext" },
{ name = "types-pyyaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/01/86c921e0e19c9fa7e705bf795998dbf55eb183e7be0342a3027dc1bcbc9f/django_stubs-5.2.9.tar.gz", hash = "sha256:c192257120b08785cfe6f2f1c91f1797aceae8e9daa689c336e52c91e8f6a493", size = 257970, upload-time = "2026-01-20T23:59:27.018Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/05/4c9c419b7051eb4b350100b086be6df487f968ab672d3d370f8ccf7c3746/django_stubs-5.2.9-py3-none-any.whl", hash = "sha256:2317a7130afdaa76f6ff7f623650d7f3bf1b6c86a60f95840e14e6ec6de1a7cd", size = 508656, upload-time = "2026-01-20T23:59:25.12Z" },
]
[[package]]
name = "django-stubs-ext"
version = "5.2.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/03/9c2be939490d2282328db4611bc5956899f5ff7eabc3e88bd4b964a87373/django_stubs_ext-5.2.9.tar.gz", hash = "sha256:6db4054d1580657b979b7d391474719f1a978773e66c7070a5e246cd445a25a9", size = 6497, upload-time = "2026-01-20T23:58:59.462Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/f7/0d5f7d7e76fe972d9f560f687fdc0cab4db9e1624fd90728ca29b4ed7a63/django_stubs_ext-5.2.9-py3-none-any.whl", hash = "sha256:230c51575551b0165be40177f0f6805f1e3ebf799b835c85f5d64c371ca6cf71", size = 9974, upload-time = "2026-01-20T23:58:58.438Z" },
]
[[package]] [[package]]
name = "djangorestframework" name = "djangorestframework"
version = "3.16.1" version = "3.16.1"
@@ -87,9 +142,13 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "django-ratelimit" },
{ name = "django-stubs" },
{ name = "djangorestframework" }, { name = "djangorestframework" },
{ name = "djangorestframework-simplejwt" }, { name = "djangorestframework-simplejwt" },
{ name = "ipython" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-dotenv" },
{ name = "uvicorn" }, { name = "uvicorn" },
{ name = "whitenoise" }, { name = "whitenoise" },
] ]
@@ -97,13 +156,26 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "django", specifier = ">=6.0.2" }, { name = "django", specifier = ">=6.0.2" },
{ name = "django-ratelimit", specifier = ">=4.1.0" },
{ name = "django-stubs", specifier = ">=5.2.9" },
{ name = "djangorestframework", specifier = ">=3.16.1" }, { name = "djangorestframework", specifier = ">=3.16.1" },
{ name = "djangorestframework-simplejwt", specifier = ">=5.5.0" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.0" },
{ name = "ipython", specifier = ">=9.10.0" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "uvicorn", specifier = ">=0.40.0" }, { name = "uvicorn", specifier = ">=0.40.0" },
{ name = "whitenoise", specifier = ">=6.11.0" }, { name = "whitenoise", specifier = ">=6.11.0" },
] ]
[[package]]
name = "executing"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@@ -113,6 +185,114 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
] ]
[[package]]
name = "ipython"
version = "9.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "decorator" },
{ name = "ipython-pygments-lexers" },
{ name = "jedi" },
{ name = "matplotlib-inline" },
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit" },
{ name = "pygments" },
{ name = "stack-data" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" },
]
[[package]]
name = "ipython-pygments-lexers"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
]
[[package]]
name = "jedi"
version = "0.19.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "parso" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
]
[[package]]
name = "matplotlib-inline"
version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
]
[[package]]
name = "parso"
version = "0.8.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" },
]
[[package]]
name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
]
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wcwidth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]]
name = "ptyprocess"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
]
[[package]]
name = "pure-eval"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@@ -181,6 +361,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
] ]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.11.0" version = "2.11.0"
@@ -190,6 +379,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
] ]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.5" version = "0.5.5"
@@ -199,6 +397,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
] ]
[[package]]
name = "stack-data"
version = "0.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asttokens" },
{ name = "executing" },
{ name = "pure-eval" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
]
[[package]]
name = "traitlets"
version = "5.14.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20250915"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
@@ -242,6 +472,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" },
] ]
[[package]]
name = "wcwidth"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
]
[[package]] [[package]]
name = "whitenoise" name = "whitenoise"
version = "6.11.0" version = "6.11.0"