From 4877e0a7797eb12b94374eff5c490af82ff9750d Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 10 Aug 2026 10:28:53 -0500 Subject: [PATCH] Initial commit: Aurora Moments self-hosted photo/video sharing platform Django/DRF + PostgreSQL/Redis/Celery backend with accounts, library, moments, and notifications apps; React/TypeScript/Vite frontend with Instagram-style sidebar nav, feed, profiles, nested comment replies, notifications, and photo/video upload with ffmpeg-generated posters. Co-Authored-By: Claude Sonnet 5 --- .env.example | 30 + .gitignore | 28 + README.md | 103 ++ api/.dockerignore | 7 + api/Dockerfile | 21 + api/apps/accounts/__init__.py | 0 api/apps/accounts/admin.py | 18 + api/apps/accounts/apps.py | 7 + api/apps/accounts/migrations/0001_initial.py | 69 + api/apps/accounts/migrations/__init__.py | 0 api/apps/accounts/models.py | 46 + api/apps/accounts/serializers.py | 91 ++ api/apps/accounts/urls.py | 13 + api/apps/accounts/views.py | 48 + api/apps/core/__init__.py | 0 api/apps/core/apps.py | 7 + api/apps/core/migrations/__init__.py | 0 api/apps/core/models.py | 29 + api/apps/library/__init__.py | 0 api/apps/library/admin.py | 32 + api/apps/library/apps.py | 7 + api/apps/library/migrations/0001_initial.py | 104 ++ api/apps/library/migrations/__init__.py | 0 api/apps/library/models.py | 123 ++ api/apps/library/permissions.py | 10 + api/apps/library/serializers.py | 83 + api/apps/library/tasks.py | 159 ++ api/apps/library/urls.py | 20 + api/apps/library/views.py | 67 + api/apps/moments/__init__.py | 0 api/apps/moments/admin.py | 19 + api/apps/moments/apps.py | 7 + api/apps/moments/migrations/0001_initial.py | 74 + api/apps/moments/migrations/__init__.py | 0 api/apps/moments/models.py | 85 + api/apps/moments/permissions.py | 8 + api/apps/moments/serializers.py | 103 ++ api/apps/moments/urls.py | 19 + api/apps/moments/views.py | 69 + api/apps/notifications/__init__.py | 0 api/apps/notifications/admin.py | 9 + api/apps/notifications/apps.py | 10 + .../notifications/migrations/0001_initial.py | 35 + api/apps/notifications/migrations/__init__.py | 0 api/apps/notifications/models.py | 31 + api/apps/notifications/serializers.py | 16 + api/apps/notifications/signals.py | 49 + api/apps/notifications/urls.py | 8 + api/apps/notifications/views.py | 24 + api/config/__init__.py | 3 + api/config/asgi.py | 7 + api/config/celery.py | 9 + api/config/settings.py | 146 ++ api/config/urls.py | 18 + api/config/wsgi.py | 7 + api/manage.py | 20 + api/requirements.txt | 13 + docker-compose.yml | 66 + mobile/README.md | 10 + web/.dockerignore | 3 + web/.gitignore | 24 + web/.oxlintrc.json | 8 + web/Dockerfile | 12 + web/README.md | 32 + web/index.html | 13 + web/package-lock.json | 1385 +++++++++++++++++ web/package.json | 26 + web/public/favicon.png | Bin 0 -> 6613 bytes web/public/favicon.svg | 1 + web/public/icons.svg | 24 + web/public/logo.png | Bin 0 -> 144833 bytes web/src/App.tsx | 81 + web/src/api/accounts.ts | 22 + web/src/api/auth.ts | 25 + web/src/api/client.ts | 99 ++ web/src/api/library.ts | 14 + web/src/api/moments.ts | 47 + web/src/api/notifications.ts | 14 + web/src/api/types.ts | 100 ++ web/src/assets/vite.svg | 1 + web/src/components/Avatar.tsx | 7 + web/src/components/MomentCard.tsx | 249 +++ web/src/components/ProtectedRoute.tsx | 11 + web/src/components/Sidebar.tsx | 82 + web/src/components/icons.tsx | 59 + web/src/context/AuthContext.tsx | 77 + web/src/index.css | 766 +++++++++ web/src/main.tsx | 10 + web/src/pages/CreatePage.tsx | 158 ++ web/src/pages/FeedPage.tsx | 33 + web/src/pages/LibraryPage.tsx | 120 ++ web/src/pages/LoginPage.tsx | 51 + web/src/pages/NotificationsPage.tsx | 57 + web/src/pages/ProfilePage.tsx | 152 ++ web/src/pages/RegisterPage.tsx | 63 + web/src/pages/SettingsPage.tsx | 98 ++ web/src/utils/formatRelativeTime.ts | 25 + web/tsconfig.app.json | 26 + web/tsconfig.json | 7 + web/tsconfig.node.json | 23 + web/vite.config.ts | 15 + 101 files changed, 6007 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api/.dockerignore create mode 100644 api/Dockerfile create mode 100644 api/apps/accounts/__init__.py create mode 100644 api/apps/accounts/admin.py create mode 100644 api/apps/accounts/apps.py create mode 100644 api/apps/accounts/migrations/0001_initial.py create mode 100644 api/apps/accounts/migrations/__init__.py create mode 100644 api/apps/accounts/models.py create mode 100644 api/apps/accounts/serializers.py create mode 100644 api/apps/accounts/urls.py create mode 100644 api/apps/accounts/views.py create mode 100644 api/apps/core/__init__.py create mode 100644 api/apps/core/apps.py create mode 100644 api/apps/core/migrations/__init__.py create mode 100644 api/apps/core/models.py create mode 100644 api/apps/library/__init__.py create mode 100644 api/apps/library/admin.py create mode 100644 api/apps/library/apps.py create mode 100644 api/apps/library/migrations/0001_initial.py create mode 100644 api/apps/library/migrations/__init__.py create mode 100644 api/apps/library/models.py create mode 100644 api/apps/library/permissions.py create mode 100644 api/apps/library/serializers.py create mode 100644 api/apps/library/tasks.py create mode 100644 api/apps/library/urls.py create mode 100644 api/apps/library/views.py create mode 100644 api/apps/moments/__init__.py create mode 100644 api/apps/moments/admin.py create mode 100644 api/apps/moments/apps.py create mode 100644 api/apps/moments/migrations/0001_initial.py create mode 100644 api/apps/moments/migrations/__init__.py create mode 100644 api/apps/moments/models.py create mode 100644 api/apps/moments/permissions.py create mode 100644 api/apps/moments/serializers.py create mode 100644 api/apps/moments/urls.py create mode 100644 api/apps/moments/views.py create mode 100644 api/apps/notifications/__init__.py create mode 100644 api/apps/notifications/admin.py create mode 100644 api/apps/notifications/apps.py create mode 100644 api/apps/notifications/migrations/0001_initial.py create mode 100644 api/apps/notifications/migrations/__init__.py create mode 100644 api/apps/notifications/models.py create mode 100644 api/apps/notifications/serializers.py create mode 100644 api/apps/notifications/signals.py create mode 100644 api/apps/notifications/urls.py create mode 100644 api/apps/notifications/views.py create mode 100644 api/config/__init__.py create mode 100644 api/config/asgi.py create mode 100644 api/config/celery.py create mode 100644 api/config/settings.py create mode 100644 api/config/urls.py create mode 100644 api/config/wsgi.py create mode 100644 api/manage.py create mode 100644 api/requirements.txt create mode 100644 docker-compose.yml create mode 100644 mobile/README.md create mode 100644 web/.dockerignore create mode 100644 web/.gitignore create mode 100644 web/.oxlintrc.json create mode 100644 web/Dockerfile create mode 100644 web/README.md create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/favicon.png create mode 100644 web/public/favicon.svg create mode 100644 web/public/icons.svg create mode 100644 web/public/logo.png create mode 100644 web/src/App.tsx create mode 100644 web/src/api/accounts.ts create mode 100644 web/src/api/auth.ts create mode 100644 web/src/api/client.ts create mode 100644 web/src/api/library.ts create mode 100644 web/src/api/moments.ts create mode 100644 web/src/api/notifications.ts create mode 100644 web/src/api/types.ts create mode 100644 web/src/assets/vite.svg create mode 100644 web/src/components/Avatar.tsx create mode 100644 web/src/components/MomentCard.tsx create mode 100644 web/src/components/ProtectedRoute.tsx create mode 100644 web/src/components/Sidebar.tsx create mode 100644 web/src/components/icons.tsx create mode 100644 web/src/context/AuthContext.tsx create mode 100644 web/src/index.css create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/CreatePage.tsx create mode 100644 web/src/pages/FeedPage.tsx create mode 100644 web/src/pages/LibraryPage.tsx create mode 100644 web/src/pages/LoginPage.tsx create mode 100644 web/src/pages/NotificationsPage.tsx create mode 100644 web/src/pages/ProfilePage.tsx create mode 100644 web/src/pages/RegisterPage.tsx create mode 100644 web/src/pages/SettingsPage.tsx create mode 100644 web/src/utils/formatRelativeTime.ts create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d821679 --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# Copy to .env and adjust for your environment. +# docker-compose reads this file automatically. + +POSTGRES_DB=aurora_moments +POSTGRES_USER=aurora +POSTGRES_PASSWORD=aurora +DATABASE_URL=postgres://aurora:aurora@postgres:5432/aurora_moments + +REDIS_URL=redis://redis:6379/0 + +DJANGO_SECRET_KEY=change-me-in-production +DEBUG=True +DJANGO_ALLOWED_HOSTS=* +CORS_ALLOWED_ORIGINS=http://localhost:5173 + +# Where original photos/videos and derived renditions are stored. +# filesystem: written under MEDIA_ROOT below (bind-mount this to your NAS +# path in production, e.g. /srv/aurora-moments/storage). +# s3: any S3-compatible object store (AWS S3, MinIO, Backblaze B2, ...). +MEDIA_BACKEND=filesystem +MEDIA_ROOT=/app/media + +# Only used when MEDIA_BACKEND=s3 +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_STORAGE_BUCKET_NAME=aurora-moments +AWS_S3_ENDPOINT_URL= +AWS_S3_REGION_NAME=us-east-1 + +VITE_API_BASE_URL=http://localhost:8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1f85be --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Python / Django +__pycache__/ +*.py[cod] +*.sqlite3 +.venv/ +venv/ +api/media/ +api/staticfiles/ +api/.env + +# Node / web +web/node_modules/ +web/dist/ +web/.env + +# Mobile +mobile/node_modules/ +mobile/ios/Pods/ +mobile/.expo/ + +# Env files +.env +!.env.example + +# Editors / OS +.DS_Store +.vscode/ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..263e28f --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# Aurora Moments + +> Aurora Moments is not a social network that stores your pictures. It's your +> photo library with a social network built around it. + +A self-hostable photo/video platform: a private, full-resolution photo +library (like iCloud Photos) with an explicit, opt-in social layer +("Moments") built on top of it. Uploading never equals publishing — a photo +only enters the social graph when its owner chooses to share it as a Moment. + +## Architecture + +``` +docker-compose +│ +├── postgres PostgreSQL — primary datastore +├── redis Celery broker/result backend +├── api Django + Django REST Framework +├── worker Celery worker (thumbnail/feed/large renditions, EXIF) +└── web React + TypeScript (Vite), talks to the API over REST +``` + +- **api/** — Django project. Apps: `accounts` (users, follows), + `library` (private originals, renditions, albums), `moments` (the social + posts, comments, reactions). JWT auth via `djangorestframework-simplejwt`. +- **web/** — React + TypeScript SPA (Vite). Login/Register, a private + Library grid with upload, and a Home feed of Moments. +- **mobile/** — not started; see [mobile/README.md](mobile/README.md). + +### Storage + +Original uploads and derived renditions (thumbnail/feed/large) go through +`MEDIA_BACKEND` in `.env`: + +- `filesystem` (default) — written to `MEDIA_ROOT`, which you can bind-mount + to a NAS path in a self-hosted deployment. +- `s3` — any S3-compatible object store (AWS S3, MinIO, Backblaze B2, ...) + via `django-storages`, no code changes needed. + +### Privacy model + +- Everything uploaded to a user's Library is private by default. +- A **Moment** is one or more Library items + caption/location, shared with + an explicit `audience`: `public`, `followers`, `friends`, `family`, + `custom`, or `only_me`. +- `family` and `custom` currently fall back to owner-only visibility — full + circle/custom-audience membership is a V1.5 feature (see + `apps.moments.models.MomentQuerySet.visible_to`); shipping that stub now + was a deliberate choice to never over-share by default. +- Shared Albums (`apps.library.models.Album` + `AlbumCollaborator`) let + multiple people contribute full-resolution photos to one place (e.g. a + wedding), without the audience/feed logic of Moments. + +## Running locally + +```bash +cp .env.example .env +docker compose up --build +``` + +- Web: http://localhost:5173 +- API: http://localhost:8000/api/ +- Django admin: http://localhost:8000/admin/ (create a superuser first, see below) + +Create a superuser for the Django admin: + +```bash +docker compose exec api python manage.py createsuperuser +``` + +Migrations run automatically on `api` container start. + +## What's in V1 + +- Registration/login (JWT), profiles, follow/followers +- Private full-resolution photo/video upload with async thumbnail/feed/large + rendition generation and EXIF extraction (Celery + Pillow) +- Share a Moment from existing Library items, with audience controls +- Home feed, comments, reactions (likes) +- Shared Albums with per-user roles (viewer/contributor/admin) +- Original-quality downloads (the original file is never deleted or resized + in place) + +Deliberately **not** in V1 (see the original concept notes this repo is +based on): automatic iOS backup, push notifications, messaging, Memories, +maps, facial recognition, semantic AI search, Stories, video transcoding, +and server federation. Those come in V1.5/V2 once the core loop — private +library, explicit sharing — is proven. + +## Backend layout + +``` +api/ +├── config/ settings, urls, celery app +└── apps/ + ├── core/ shared abstract models (TimeStampedModel, Audience enum) + ├── accounts/ User, Follow + ├── library/ LibraryItem, MediaRendition, Album, AlbumItem, AlbumCollaborator + └── moments/ Moment, MomentItem, Comment, Reaction +``` + +REST API root: `/api/accounts/`, `/api/library/`, `/api/moments/`, +`/api/auth/token/` (JWT obtain/refresh). diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..9df72ee --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,7 @@ +__pycache__ +*.pyc +.venv +venv +media +staticfiles +.env diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..bce5ec9 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libpq-dev gcc ffmpeg \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN mkdir -p /app/media /app/staticfiles + +EXPOSE 8000 + +CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/api/apps/accounts/__init__.py b/api/apps/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/accounts/admin.py b/api/apps/accounts/admin.py new file mode 100644 index 0000000..9b90c49 --- /dev/null +++ b/api/apps/accounts/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin + +from .models import Follow, User + + +@admin.register(User) +class UserAdmin(DjangoUserAdmin): + fieldsets = DjangoUserAdmin.fieldsets + ( + ("Aurora Moments profile", {"fields": ("display_name", "bio", "avatar", "is_private")}), + ) + list_display = ("username", "email", "display_name", "is_private", "is_staff") + + +@admin.register(Follow) +class FollowAdmin(admin.ModelAdmin): + list_display = ("follower", "following", "status", "created_at") + list_filter = ("status",) diff --git a/api/apps/accounts/apps.py b/api/apps/accounts/apps.py new file mode 100644 index 0000000..7d07530 --- /dev/null +++ b/api/apps/accounts/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.accounts" + label = "accounts" diff --git a/api/apps/accounts/migrations/0001_initial.py b/api/apps/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..e55286b --- /dev/null +++ b/api/apps/accounts/migrations/0001_initial.py @@ -0,0 +1,69 @@ +# Generated by Django 5.0.9 on 2026-08-09 17:00 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True)), + ('display_name', models.CharField(blank=True, max_length=150)), + ('bio', models.TextField(blank=True, max_length=500)), + ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')), + ('is_private', models.BooleanField(default=False, help_text='If true, new followers must be approved before they can see followers-only Moments.')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Follow', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted')], default='accepted', max_length=10)), + ('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following_edges', to=settings.AUTH_USER_MODEL)), + ('following', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='follower_edges', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddConstraint( + model_name='follow', + constraint=models.CheckConstraint(check=models.Q(('follower', models.F('following')), _negated=True), name='follow_no_self_follow'), + ), + migrations.AlterUniqueTogether( + name='follow', + unique_together={('follower', 'following')}, + ), + ] diff --git a/api/apps/accounts/migrations/__init__.py b/api/apps/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/accounts/models.py b/api/apps/accounts/models.py new file mode 100644 index 0000000..c9c84c7 --- /dev/null +++ b/api/apps/accounts/models.py @@ -0,0 +1,46 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + +from apps.core.models import TimeStampedModel + + +class User(AbstractUser): + """Aurora Moments account. Email is the login identifier; username is the + public @handle shown on profiles.""" + + email = models.EmailField(unique=True) + display_name = models.CharField(max_length=150, blank=True) + bio = models.TextField(max_length=500, blank=True) + avatar = models.ImageField(upload_to="avatars/", blank=True, null=True) + is_private = models.BooleanField( + default=False, + help_text="If true, new followers must be approved before they can see followers-only Moments.", + ) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["username"] + + def __str__(self): + return self.username + + +class Follow(TimeStampedModel): + class Status(models.TextChoices): + PENDING = "pending", "Pending" + ACCEPTED = "accepted", "Accepted" + + follower = models.ForeignKey(User, related_name="following_edges", on_delete=models.CASCADE) + following = models.ForeignKey(User, related_name="follower_edges", on_delete=models.CASCADE) + status = models.CharField(max_length=10, choices=Status.choices, default=Status.ACCEPTED) + + class Meta: + unique_together = ("follower", "following") + constraints = [ + models.CheckConstraint( + check=~models.Q(follower=models.F("following")), + name="follow_no_self_follow", + ) + ] + + def __str__(self): + return f"{self.follower} -> {self.following} ({self.status})" diff --git a/api/apps/accounts/serializers.py b/api/apps/accounts/serializers.py new file mode 100644 index 0000000..6346fdc --- /dev/null +++ b/api/apps/accounts/serializers.py @@ -0,0 +1,91 @@ +from django.contrib.auth import password_validation +from rest_framework import serializers + +from .models import Follow, User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "username", "display_name", "bio", "avatar", "is_private", "date_joined"] + read_only_fields = ["id", "date_joined"] + + +class ProfileSerializer(UserSerializer): + """UserSerializer plus the counts/relationship state a profile page + needs. Kept separate from UserSerializer (used for owner/author fields + all over the feed) so those hot paths don't pay for two extra COUNT + queries per embedded user.""" + + follower_count = serializers.SerializerMethodField() + following_count = serializers.SerializerMethodField() + viewer_follow_id = serializers.SerializerMethodField() + viewer_follow_status = serializers.SerializerMethodField() + + class Meta(UserSerializer.Meta): + fields = UserSerializer.Meta.fields + [ + "follower_count", "following_count", "viewer_follow_id", "viewer_follow_status", + ] + + def _viewer_follow(self, obj): + request = self.context.get("request") + if not request or not request.user.is_authenticated or request.user.id == obj.id: + return None + if not hasattr(obj, "_viewer_follow_cache"): + obj._viewer_follow_cache = Follow.objects.filter(follower=request.user, following=obj).first() + return obj._viewer_follow_cache + + def get_follower_count(self, obj): + return obj.follower_edges.filter(status=Follow.Status.ACCEPTED).count() + + def get_following_count(self, obj): + return obj.following_edges.filter(status=Follow.Status.ACCEPTED).count() + + def get_viewer_follow_id(self, obj): + follow = self._viewer_follow(obj) + return follow.id if follow else None + + def get_viewer_follow_status(self, obj): + follow = self._viewer_follow(obj) + return follow.status if follow else None + + +class RegisterSerializer(serializers.ModelSerializer): + password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ["id", "email", "username", "password", "display_name"] + + def validate_password(self, value): + password_validation.validate_password(value) + return value + + def create(self, validated_data): + password = validated_data.pop("password") + user = User(**validated_data) + user.set_password(password) + user.save() + return user + + +class FollowSerializer(serializers.ModelSerializer): + follower = UserSerializer(read_only=True) + following = UserSerializer(read_only=True) + following_id = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), source="following", write_only=True + ) + + class Meta: + model = Follow + fields = ["id", "follower", "following", "following_id", "status", "created_at"] + read_only_fields = ["id", "follower", "status", "created_at"] + + def create(self, validated_data): + follower = self.context["request"].user + following = validated_data["following"] + status = Follow.Status.PENDING if following.is_private else Follow.Status.ACCEPTED + follow, _ = Follow.objects.get_or_create( + follower=follower, following=following, defaults={"status": status} + ) + return follow diff --git a/api/apps/accounts/urls.py b/api/apps/accounts/urls.py new file mode 100644 index 0000000..bee9c5d --- /dev/null +++ b/api/apps/accounts/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter + +from .views import FollowViewSet, MeView, RegisterView, UserViewSet + +router = DefaultRouter() +router.register("users", UserViewSet, basename="user") +router.register("follows", FollowViewSet, basename="follow") + +urlpatterns = [ + path("register/", RegisterView.as_view(), name="register"), + path("me/", MeView.as_view(), name="me"), +] + router.urls diff --git a/api/apps/accounts/views.py b/api/apps/accounts/views.py new file mode 100644 index 0000000..734b019 --- /dev/null +++ b/api/apps/accounts/views.py @@ -0,0 +1,48 @@ +from rest_framework import generics, permissions, viewsets +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response +from rest_framework.views import APIView + +from .models import Follow, User +from .serializers import FollowSerializer, ProfileSerializer, RegisterSerializer, UserSerializer + + +class RegisterView(generics.CreateAPIView): + queryset = User.objects.all() + serializer_class = RegisterSerializer + permission_classes = [permissions.AllowAny] + + +class MeView(APIView): + def get(self, request): + return Response(ProfileSerializer(request.user, context={"request": request}).data) + + def patch(self, request): + serializer = UserSerializer(request.user, data=request.data, partial=True, context={"request": request}) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + +class UserViewSet(viewsets.ReadOnlyModelViewSet): + queryset = User.objects.all() + serializer_class = ProfileSerializer + lookup_field = "username" + + +class FollowViewSet(viewsets.ModelViewSet): + serializer_class = FollowSerializer + http_method_names = ["get", "post", "delete", "head", "options"] + + def get_queryset(self): + user = self.request.user + qs = Follow.objects.select_related("follower", "following") + direction = self.request.query_params.get("direction", "following") + if direction == "followers": + return qs.filter(following=user) + return qs.filter(follower=user) + + def perform_destroy(self, instance): + if instance.follower_id != self.request.user.id: + raise PermissionDenied("Cannot delete another user's follow edge.") + instance.delete() diff --git a/api/apps/core/__init__.py b/api/apps/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/core/apps.py b/api/apps/core/apps.py new file mode 100644 index 0000000..68868e8 --- /dev/null +++ b/api/apps/core/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.core" + label = "core" diff --git a/api/apps/core/migrations/__init__.py b/api/apps/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/core/models.py b/api/apps/core/models.py new file mode 100644 index 0000000..960d13e --- /dev/null +++ b/api/apps/core/models.py @@ -0,0 +1,29 @@ +import uuid + +from django.db import models + + +class TimeStampedModel(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class UUIDModel(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + class Meta: + abstract = True + + +class Audience(models.TextChoices): + """Who can see a Moment or Album. Mirrors the concept doc's privacy model.""" + + PUBLIC = "public", "Public" + FOLLOWERS = "followers", "Followers" + FRIENDS = "friends", "Friends" + FAMILY = "family", "Family" + CUSTOM = "custom", "Custom" + ONLY_ME = "only_me", "Only Me" diff --git a/api/apps/library/__init__.py b/api/apps/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/library/admin.py b/api/apps/library/admin.py new file mode 100644 index 0000000..d802703 --- /dev/null +++ b/api/apps/library/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin + +from .models import Album, AlbumCollaborator, AlbumItem, LibraryItem, MediaRendition + + +class MediaRenditionInline(admin.TabularInline): + model = MediaRendition + extra = 0 + + +@admin.register(LibraryItem) +class LibraryItemAdmin(admin.ModelAdmin): + list_display = ("original_filename", "owner", "media_type", "processing_status", "captured_at") + list_filter = ("media_type", "processing_status") + search_fields = ("original_filename", "owner__username") + inlines = [MediaRenditionInline] + + +class AlbumItemInline(admin.TabularInline): + model = AlbumItem + extra = 0 + + +class AlbumCollaboratorInline(admin.TabularInline): + model = AlbumCollaborator + extra = 0 + + +@admin.register(Album) +class AlbumAdmin(admin.ModelAdmin): + list_display = ("name", "owner", "audience", "created_at") + inlines = [AlbumItemInline, AlbumCollaboratorInline] diff --git a/api/apps/library/apps.py b/api/apps/library/apps.py new file mode 100644 index 0000000..2c20c1b --- /dev/null +++ b/api/apps/library/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class LibraryConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.library" + label = "library" diff --git a/api/apps/library/migrations/0001_initial.py b/api/apps/library/migrations/0001_initial.py new file mode 100644 index 0000000..45af85a --- /dev/null +++ b/api/apps/library/migrations/0001_initial.py @@ -0,0 +1,104 @@ +# Generated by Django 5.0.9 on 2026-08-09 17:00 + +import apps.library.models +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='LibraryItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('file', models.FileField(upload_to=apps.library.models.library_upload_path)), + ('media_type', models.CharField(choices=[('photo', 'Photo'), ('video', 'Video')], max_length=10)), + ('original_filename', models.CharField(max_length=255)), + ('file_size', models.PositiveBigIntegerField(default=0)), + ('width', models.PositiveIntegerField(blank=True, null=True)), + ('height', models.PositiveIntegerField(blank=True, null=True)), + ('duration_seconds', models.FloatField(blank=True, null=True)), + ('captured_at', models.DateTimeField(blank=True, null=True)), + ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('camera_make', models.CharField(blank=True, max_length=100)), + ('camera_model', models.CharField(blank=True, max_length=100)), + ('checksum', models.CharField(db_index=True, help_text='sha256 of the original file, used for dedup', max_length=64)), + ('processing_status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('ready', 'Ready'), ('failed', 'Failed')], default='pending', max_length=12)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='library_items', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-captured_at', '-created_at'], + 'unique_together': {('owner', 'checksum')}, + }, + ), + migrations.CreateModel( + name='Album', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=150)), + ('description', models.TextField(blank=True)), + ('audience', models.CharField(choices=[('public', 'Public'), ('followers', 'Followers'), ('friends', 'Friends'), ('family', 'Family'), ('custom', 'Custom'), ('only_me', 'Only Me')], default='only_me', max_length=10)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='albums', to=settings.AUTH_USER_MODEL)), + ('cover_item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='library.libraryitem')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AlbumCollaborator', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('viewer', 'Viewer'), ('contributor', 'Contributor'), ('admin', 'Admin')], default='viewer', max_length=11)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted')], default='pending', max_length=8)), + ('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collaborators', to='library.album')), + ('invited_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='album_invites', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('album', 'user')}, + }, + ), + migrations.CreateModel( + name='AlbumItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('added_at', models.DateTimeField(auto_now_add=True)), + ('added_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)), + ('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='library.album')), + ('library_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='album_items', to='library.libraryitem')), + ], + options={ + 'ordering': ['-added_at'], + 'unique_together': {('album', 'library_item')}, + }, + ), + migrations.CreateModel( + name='MediaRendition', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('kind', models.CharField(choices=[('thumbnail', 'Thumbnail'), ('feed', 'Feed'), ('large', 'Large / Fullscreen')], max_length=10)), + ('file', models.FileField(upload_to=apps.library.models.rendition_upload_path)), + ('width', models.PositiveIntegerField()), + ('height', models.PositiveIntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('library_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='renditions', to='library.libraryitem')), + ], + options={ + 'unique_together': {('library_item', 'kind')}, + }, + ), + ] diff --git a/api/apps/library/migrations/__init__.py b/api/apps/library/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/library/models.py b/api/apps/library/models.py new file mode 100644 index 0000000..db1a03f --- /dev/null +++ b/api/apps/library/models.py @@ -0,0 +1,123 @@ +from django.conf import settings +from django.db import models + +from apps.core.models import Audience, TimeStampedModel + + +def library_upload_path(instance, filename): + return f"library/{instance.owner_id}/originals/{filename}" + + +def rendition_upload_path(instance, filename): + return f"library/{instance.library_item.owner_id}/renditions/{instance.kind}/{filename}" + + +class LibraryItem(TimeStampedModel): + """A single photo or video backed up to a user's private library. + Nothing here is visible to anyone but the owner until it is attached to + a Moment or a shared Album.""" + + class MediaType(models.TextChoices): + PHOTO = "photo", "Photo" + VIDEO = "video", "Video" + + class ProcessingStatus(models.TextChoices): + PENDING = "pending", "Pending" + PROCESSING = "processing", "Processing" + READY = "ready", "Ready" + FAILED = "failed", "Failed" + + owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="library_items", on_delete=models.CASCADE) + file = models.FileField(upload_to=library_upload_path) + media_type = models.CharField(max_length=10, choices=MediaType.choices) + original_filename = models.CharField(max_length=255) + file_size = models.PositiveBigIntegerField(default=0) + width = models.PositiveIntegerField(null=True, blank=True) + height = models.PositiveIntegerField(null=True, blank=True) + duration_seconds = models.FloatField(null=True, blank=True) + + captured_at = models.DateTimeField(null=True, blank=True) + latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + camera_make = models.CharField(max_length=100, blank=True) + camera_model = models.CharField(max_length=100, blank=True) + + checksum = models.CharField(max_length=64, db_index=True, help_text="sha256 of the original file, used for dedup") + processing_status = models.CharField( + max_length=12, choices=ProcessingStatus.choices, default=ProcessingStatus.PENDING + ) + + class Meta: + unique_together = ("owner", "checksum") + ordering = ["-captured_at", "-created_at"] + + def __str__(self): + return f"{self.original_filename} ({self.owner})" + + +class MediaRendition(models.Model): + """A derived, smaller version of a LibraryItem (thumbnail/feed/large), + generated asynchronously so the original never has to be served to the + browser just to render a grid.""" + + class Kind(models.TextChoices): + THUMBNAIL = "thumbnail", "Thumbnail" + FEED = "feed", "Feed" + LARGE = "large", "Large / Fullscreen" + + library_item = models.ForeignKey(LibraryItem, related_name="renditions", on_delete=models.CASCADE) + kind = models.CharField(max_length=10, choices=Kind.choices) + file = models.FileField(upload_to=rendition_upload_path) + width = models.PositiveIntegerField() + height = models.PositiveIntegerField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("library_item", "kind") + + def __str__(self): + return f"{self.library_item_id} - {self.kind}" + + +class Album(TimeStampedModel): + owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="albums", on_delete=models.CASCADE) + name = models.CharField(max_length=150) + description = models.TextField(blank=True) + cover_item = models.ForeignKey( + LibraryItem, related_name="+", on_delete=models.SET_NULL, null=True, blank=True + ) + audience = models.CharField(max_length=10, choices=Audience.choices, default=Audience.ONLY_ME) + + def __str__(self): + return self.name + + +class AlbumItem(models.Model): + album = models.ForeignKey(Album, related_name="items", on_delete=models.CASCADE) + library_item = models.ForeignKey(LibraryItem, related_name="album_items", on_delete=models.CASCADE) + added_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE) + added_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("album", "library_item") + ordering = ["-added_at"] + + +class AlbumCollaborator(models.Model): + class Role(models.TextChoices): + VIEWER = "viewer", "Viewer" + CONTRIBUTOR = "contributor", "Contributor" + ADMIN = "admin", "Admin" + + class Status(models.TextChoices): + PENDING = "pending", "Pending" + ACCEPTED = "accepted", "Accepted" + + album = models.ForeignKey(Album, related_name="collaborators", on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="album_invites", on_delete=models.CASCADE) + role = models.CharField(max_length=11, choices=Role.choices, default=Role.VIEWER) + invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE) + status = models.CharField(max_length=8, choices=Status.choices, default=Status.PENDING) + + class Meta: + unique_together = ("album", "user") diff --git a/api/apps/library/permissions.py b/api/apps/library/permissions.py new file mode 100644 index 0000000..be76265 --- /dev/null +++ b/api/apps/library/permissions.py @@ -0,0 +1,10 @@ +from rest_framework.permissions import BasePermission + + +class IsOwner(BasePermission): + """Library items and albums are private by default — only the owner may + read or write them through these endpoints. Sharing happens explicitly + via Moments / AlbumCollaborators, handled by their own views.""" + + def has_object_permission(self, request, view, obj): + return obj.owner_id == request.user.id diff --git a/api/apps/library/serializers.py b/api/apps/library/serializers.py new file mode 100644 index 0000000..0f8a9ec --- /dev/null +++ b/api/apps/library/serializers.py @@ -0,0 +1,83 @@ +import hashlib + +from rest_framework import serializers + +from apps.accounts.models import User +from apps.accounts.serializers import UserSerializer + +from .models import Album, AlbumCollaborator, AlbumItem, LibraryItem, MediaRendition + + +class MediaRenditionSerializer(serializers.ModelSerializer): + class Meta: + model = MediaRendition + fields = ["kind", "file", "width", "height"] + + +class LibraryItemSerializer(serializers.ModelSerializer): + renditions = MediaRenditionSerializer(many=True, read_only=True) + + class Meta: + model = LibraryItem + fields = [ + "id", "file", "media_type", "original_filename", "file_size", + "width", "height", "duration_seconds", "captured_at", + "latitude", "longitude", "camera_make", "camera_model", + "processing_status", "renditions", "created_at", + ] + read_only_fields = [ + "id", "width", "height", "duration_seconds", "captured_at", + "camera_make", "camera_model", "processing_status", "renditions", "created_at", + ] + + def create(self, validated_data): + upload = validated_data["file"] + checksum = hashlib.sha256(upload.read()).hexdigest() + upload.seek(0) + validated_data["checksum"] = checksum + validated_data["file_size"] = upload.size + validated_data.setdefault("original_filename", upload.name) + validated_data["owner"] = self.context["request"].user + library_item, _created = LibraryItem.objects.get_or_create( + owner=validated_data["owner"], + checksum=checksum, + defaults=validated_data, + ) + return library_item + + +class AlbumItemSerializer(serializers.ModelSerializer): + library_item = LibraryItemSerializer(read_only=True) + library_item_id = serializers.PrimaryKeyRelatedField( + queryset=LibraryItem.objects.all(), source="library_item", write_only=True + ) + + class Meta: + model = AlbumItem + fields = ["id", "album", "library_item", "library_item_id", "added_by", "added_at"] + read_only_fields = ["id", "album", "added_by", "added_at"] + + +class AlbumCollaboratorSerializer(serializers.ModelSerializer): + user = UserSerializer(read_only=True) + user_id = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), source="user", write_only=True + ) + + class Meta: + model = AlbumCollaborator + fields = ["id", "album", "user", "user_id", "role", "status", "invited_by"] + read_only_fields = ["id", "album", "status", "invited_by"] + + +class AlbumSerializer(serializers.ModelSerializer): + items = AlbumItemSerializer(many=True, read_only=True) + collaborators = AlbumCollaboratorSerializer(many=True, read_only=True) + + class Meta: + model = Album + fields = [ + "id", "owner", "name", "description", "cover_item", "audience", + "items", "collaborators", "created_at", "updated_at", + ] + read_only_fields = ["id", "owner", "items", "collaborators", "created_at", "updated_at"] diff --git a/api/apps/library/tasks.py b/api/apps/library/tasks.py new file mode 100644 index 0000000..38e8720 --- /dev/null +++ b/api/apps/library/tasks.py @@ -0,0 +1,159 @@ +import io +import json +import os +import subprocess +import tempfile +from datetime import datetime + +from celery import shared_task +from django.core.files.base import ContentFile +from PIL import ExifTags, Image + +from .models import LibraryItem, MediaRendition + +RENDITION_MAX_DIMENSIONS = { + MediaRendition.Kind.THUMBNAIL: 300, + MediaRendition.Kind.FEED: 1080, + MediaRendition.Kind.LARGE: 2048, +} + + +@shared_task +def process_library_item(library_item_id): + try: + item = LibraryItem.objects.get(id=library_item_id) + except LibraryItem.DoesNotExist: + return + + item.processing_status = LibraryItem.ProcessingStatus.PROCESSING + item.save(update_fields=["processing_status"]) + + try: + if item.media_type == LibraryItem.MediaType.PHOTO: + _process_photo(item) + else: + # Full transcoding to web-friendly renditions is a V1.5 feature + # (see README); the original file is served as-is for playback. + # We do extract dimensions/duration and a poster-frame thumbnail + # so videos aren't blank tiles in the UI. + _process_video(item) + item.processing_status = LibraryItem.ProcessingStatus.READY + except Exception: + item.processing_status = LibraryItem.ProcessingStatus.FAILED + raise + finally: + item.save(update_fields=["processing_status"]) + + +def _process_photo(item): + with item.file.open("rb") as fh: + image = Image.open(fh) + image.load() + + for field, value in _extract_exif(image).items(): + setattr(item, field, value) + item.width, item.height = image.size + item.save() + + _write_renditions(item, image) + + +def _process_video(item): + suffix = os.path.splitext(item.original_filename)[1] or ".mp4" + with tempfile.NamedTemporaryFile(suffix=suffix) as src: + with item.file.open("rb") as fh: + for chunk in fh.chunks(): + src.write(chunk) + src.flush() + + probe = _ffprobe(src.name) + if probe: + item.width = probe.get("width") or item.width + item.height = probe.get("height") or item.height + item.duration_seconds = probe.get("duration") + item.save() + + # A missing/corrupt frame just means no poster image; the video + # itself still uploaded fine, so this must not fail the whole job. + try: + frame_bytes = _extract_poster_frame(src.name, probe.get("duration") if probe else None) + if frame_bytes: + image = Image.open(io.BytesIO(frame_bytes)) + image.load() + _write_renditions(item, image) + except Exception: + pass + + +def _write_renditions(item, image): + for kind, max_dimension in RENDITION_MAX_DIMENSIONS.items(): + rendition_image = image.copy() + rendition_image.thumbnail((max_dimension, max_dimension)) + rgb_image = rendition_image.convert("RGB") + buffer = io.BytesIO() + rgb_image.save(buffer, format="JPEG", quality=85) + MediaRendition.objects.update_or_create( + library_item=item, + kind=kind, + defaults={ + "width": rendition_image.width, + "height": rendition_image.height, + "file": ContentFile(buffer.getvalue(), name=f"{item.id}_{kind}.jpg"), + }, + ) + + +def _ffprobe(path): + try: + result = subprocess.run( + [ + "ffprobe", "-v", "error", "-select_streams", "v:0", + "-show_entries", "stream=width,height:format=duration", + "-of", "json", path, + ], + capture_output=True, text=True, timeout=30, check=True, + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + return None + data = json.loads(result.stdout) + streams = data.get("streams") or [{}] + duration = (data.get("format") or {}).get("duration") + return { + "width": streams[0].get("width"), + "height": streams[0].get("height"), + "duration": float(duration) if duration else None, + } + + +def _extract_poster_frame(path, duration_seconds): + timestamp = min(1.0, duration_seconds / 2) if duration_seconds else 0.1 + with tempfile.NamedTemporaryFile(suffix=".jpg") as out: + try: + subprocess.run( + ["ffmpeg", "-y", "-ss", str(timestamp), "-i", path, "-frames:v", "1", "-q:v", "3", out.name], + capture_output=True, timeout=30, check=True, + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + return None + return out.read() + + +def _extract_exif(image): + result = {} + raw_exif = image.getexif() + if not raw_exif: + return result + tags = {ExifTags.TAGS.get(tag_id, tag_id): value for tag_id, value in raw_exif.items()} + + if tags.get("Make"): + result["camera_make"] = str(tags["Make"]).strip() + if tags.get("Model"): + result["camera_model"] = str(tags["Model"]).strip() + + date_str = tags.get("DateTimeOriginal") or tags.get("DateTime") + if date_str: + try: + result["captured_at"] = datetime.strptime(str(date_str), "%Y:%m:%d %H:%M:%S") + except ValueError: + pass + return result diff --git a/api/apps/library/urls.py b/api/apps/library/urls.py new file mode 100644 index 0000000..064702b --- /dev/null +++ b/api/apps/library/urls.py @@ -0,0 +1,20 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter + +from .views import AlbumCollaboratorViewSet, AlbumItemViewSet, AlbumViewSet, LibraryItemViewSet + +router = DefaultRouter() +router.register("items", LibraryItemViewSet, basename="library-item") +router.register("albums", AlbumViewSet, basename="album") + +album_items_list = AlbumItemViewSet.as_view({"get": "list", "post": "create"}) +album_items_detail = AlbumItemViewSet.as_view({"get": "retrieve", "delete": "destroy"}) +album_collaborators_list = AlbumCollaboratorViewSet.as_view({"get": "list", "post": "create"}) +album_collaborators_detail = AlbumCollaboratorViewSet.as_view({"get": "retrieve", "delete": "destroy"}) + +urlpatterns = [ + path("albums//items/", album_items_list, name="album-items-list"), + path("albums//items//", album_items_detail, name="album-items-detail"), + path("albums//collaborators/", album_collaborators_list, name="album-collaborators-list"), + path("albums//collaborators//", album_collaborators_detail, name="album-collaborators-detail"), +] + router.urls diff --git a/api/apps/library/views.py b/api/apps/library/views.py new file mode 100644 index 0000000..28bd553 --- /dev/null +++ b/api/apps/library/views.py @@ -0,0 +1,67 @@ +from rest_framework import permissions, viewsets +from rest_framework.exceptions import PermissionDenied + +from .models import Album, AlbumCollaborator, AlbumItem, LibraryItem +from .permissions import IsOwner +from .serializers import AlbumCollaboratorSerializer, AlbumItemSerializer, AlbumSerializer, LibraryItemSerializer +from .tasks import process_library_item + + +class LibraryItemViewSet(viewsets.ModelViewSet): + serializer_class = LibraryItemSerializer + permission_classes = [permissions.IsAuthenticated, IsOwner] + + def get_queryset(self): + return LibraryItem.objects.filter(owner=self.request.user) + + def perform_create(self, serializer): + item = serializer.save() + process_library_item.delay(str(item.id)) + + +class AlbumViewSet(viewsets.ModelViewSet): + serializer_class = AlbumSerializer + permission_classes = [permissions.IsAuthenticated, IsOwner] + + def get_queryset(self): + return Album.objects.filter(owner=self.request.user).prefetch_related("items", "collaborators") + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + +class AlbumItemViewSet(viewsets.ModelViewSet): + serializer_class = AlbumItemSerializer + permission_classes = [permissions.IsAuthenticated] + http_method_names = ["get", "post", "delete", "head", "options"] + + def _get_album(self): + album = Album.objects.get(pk=self.kwargs["album_pk"]) + allowed = album.owner_id == self.request.user.id or album.collaborators.filter( + user=self.request.user, role=AlbumCollaborator.Role.CONTRIBUTOR, status=AlbumCollaborator.Status.ACCEPTED + ).exists() + if not allowed: + raise PermissionDenied("Not a contributor on this album.") + return album + + def get_queryset(self): + return AlbumItem.objects.filter(album=self._get_album()) + + def perform_create(self, serializer): + serializer.save(album=self._get_album(), added_by=self.request.user) + + +class AlbumCollaboratorViewSet(viewsets.ModelViewSet): + serializer_class = AlbumCollaboratorSerializer + permission_classes = [permissions.IsAuthenticated] + http_method_names = ["get", "post", "delete", "head", "options"] + + def _get_album(self): + album = Album.objects.get(pk=self.kwargs["album_pk"], owner=self.request.user) + return album + + def get_queryset(self): + return AlbumCollaborator.objects.filter(album=self._get_album()) + + def perform_create(self, serializer): + serializer.save(album=self._get_album(), invited_by=self.request.user) diff --git a/api/apps/moments/__init__.py b/api/apps/moments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/moments/admin.py b/api/apps/moments/admin.py new file mode 100644 index 0000000..5bf3826 --- /dev/null +++ b/api/apps/moments/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin + +from .models import Comment, Moment, MomentItem, Reaction + + +class MomentItemInline(admin.TabularInline): + model = MomentItem + extra = 0 + + +@admin.register(Moment) +class MomentAdmin(admin.ModelAdmin): + list_display = ("id", "owner", "audience", "created_at") + list_filter = ("audience",) + inlines = [MomentItemInline] + + +admin.site.register(Comment) +admin.site.register(Reaction) diff --git a/api/apps/moments/apps.py b/api/apps/moments/apps.py new file mode 100644 index 0000000..0a635ab --- /dev/null +++ b/api/apps/moments/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class MomentsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.moments" + label = "moments" diff --git a/api/apps/moments/migrations/0001_initial.py b/api/apps/moments/migrations/0001_initial.py new file mode 100644 index 0000000..47479f1 --- /dev/null +++ b/api/apps/moments/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# Generated by Django 5.0.9 on 2026-08-09 17:00 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('library', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Moment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('caption', models.TextField(blank=True, max_length=2200)), + ('location', models.CharField(blank=True, max_length=255)), + ('audience', models.CharField(choices=[('public', 'Public'), ('followers', 'Followers'), ('friends', 'Friends'), ('family', 'Family'), ('custom', 'Custom'), ('only_me', 'Only Me')], default='public', max_length=10)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moments', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('body', models.TextField(max_length=1000)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL)), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='moments.comment')), + ('moment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='moments.moment')), + ], + options={ + 'ordering': ['created_at'], + }, + ), + migrations.CreateModel( + name='MomentItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(default=0)), + ('library_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='moment_items', to='library.libraryitem')), + ('moment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='moments.moment')), + ], + options={ + 'ordering': ['order'], + 'unique_together': {('moment', 'library_item')}, + }, + ), + migrations.CreateModel( + name='Reaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reaction_type', models.CharField(choices=[('like', 'Like')], default='like', max_length=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('moment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='moments.moment')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('moment', 'user')}, + }, + ), + ] diff --git a/api/apps/moments/migrations/__init__.py b/api/apps/moments/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/moments/models.py b/api/apps/moments/models.py new file mode 100644 index 0000000..a961bad --- /dev/null +++ b/api/apps/moments/models.py @@ -0,0 +1,85 @@ +from django.conf import settings +from django.db import models + +from apps.core.models import Audience, TimeStampedModel +from apps.library.models import LibraryItem + + +class MomentQuerySet(models.QuerySet): + def visible_to(self, user): + """FAMILY and CUSTOM audiences intentionally fall back to + owner-only visibility until circles/custom-audience membership + (V1.5) exists — better to under-share than leak content.""" + from apps.accounts.models import Follow + + following_ids = Follow.objects.filter( + follower=user, status=Follow.Status.ACCEPTED + ).values_list("following_id", flat=True) + follower_ids = set( + Follow.objects.filter( + following=user, status=Follow.Status.ACCEPTED + ).values_list("follower_id", flat=True) + ) + friend_ids = follower_ids & set(following_ids) + + return self.filter( + models.Q(owner=user) + | models.Q(audience=Audience.PUBLIC) + | models.Q(audience=Audience.FOLLOWERS, owner_id__in=following_ids) + | models.Q(audience=Audience.FRIENDS, owner_id__in=friend_ids) + ).distinct() + + +class Moment(TimeStampedModel): + """The social-media object: one or more LibraryItems shared with an + explicit audience. Sharing a Moment never moves or copies the original + file — it just makes the existing library item visible to others.""" + + owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="moments", on_delete=models.CASCADE) + caption = models.TextField(max_length=2200, blank=True) + location = models.CharField(max_length=255, blank=True) + audience = models.CharField(max_length=10, choices=Audience.choices, default=Audience.PUBLIC) + + objects = MomentQuerySet.as_manager() + + class Meta: + ordering = ["-created_at"] + + def __str__(self): + return f"Moment({self.id}) by {self.owner}" + + +class MomentItem(models.Model): + moment = models.ForeignKey(Moment, related_name="items", on_delete=models.CASCADE) + library_item = models.ForeignKey(LibraryItem, related_name="moment_items", on_delete=models.CASCADE) + order = models.PositiveIntegerField(default=0) + + class Meta: + ordering = ["order"] + unique_together = ("moment", "library_item") + + +class Comment(TimeStampedModel): + moment = models.ForeignKey(Moment, related_name="comments", on_delete=models.CASCADE) + author = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="comments", on_delete=models.CASCADE) + parent = models.ForeignKey("self", related_name="replies", on_delete=models.CASCADE, null=True, blank=True) + body = models.TextField(max_length=1000) + + class Meta: + ordering = ["created_at"] + + def __str__(self): + return f"{self.author}: {self.body[:40]}" + + +class Reaction(models.Model): + class ReactionType(models.TextChoices): + LIKE = "like", "Like" + + moment = models.ForeignKey(Moment, related_name="reactions", on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="reactions", on_delete=models.CASCADE) + reaction_type = models.CharField(max_length=10, choices=ReactionType.choices, default=ReactionType.LIKE) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("moment", "user") diff --git a/api/apps/moments/permissions.py b/api/apps/moments/permissions.py new file mode 100644 index 0000000..afc6fb9 --- /dev/null +++ b/api/apps/moments/permissions.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import SAFE_METHODS, BasePermission + + +class IsOwnerOrReadOnly(BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in SAFE_METHODS: + return True + return obj.owner_id == request.user.id diff --git a/api/apps/moments/serializers.py b/api/apps/moments/serializers.py new file mode 100644 index 0000000..bd7f458 --- /dev/null +++ b/api/apps/moments/serializers.py @@ -0,0 +1,103 @@ +from django.db import transaction +from rest_framework import serializers + +from apps.accounts.serializers import UserSerializer +from apps.library.models import LibraryItem +from apps.library.serializers import LibraryItemSerializer + +from .models import Comment, Moment, MomentItem, Reaction + + +class MomentItemSerializer(serializers.ModelSerializer): + library_item = LibraryItemSerializer(read_only=True) + + class Meta: + model = MomentItem + fields = ["id", "library_item", "order"] + + +class CommentSerializer(serializers.ModelSerializer): + author = UserSerializer(read_only=True) + + class Meta: + model = Comment + fields = ["id", "moment", "author", "parent", "body", "created_at"] + read_only_fields = ["id", "moment", "author", "created_at"] + + def validate_parent(self, value): + if value is None: + return value + if value.parent_id is not None: + raise serializers.ValidationError("Cannot reply to a reply; reply to the original comment instead.") + moment = self.context.get("moment") + if moment is not None and value.moment_id != moment.id: + raise serializers.ValidationError("Reply must target a comment on the same Moment.") + return value + + +class ReactionSerializer(serializers.ModelSerializer): + user = UserSerializer(read_only=True) + + class Meta: + model = Reaction + fields = ["id", "moment", "user", "reaction_type", "created_at"] + read_only_fields = ["id", "moment", "user", "created_at"] + + +class MomentSerializer(serializers.ModelSerializer): + owner = UserSerializer(read_only=True) + items = MomentItemSerializer(many=True, read_only=True) + library_item_ids = serializers.PrimaryKeyRelatedField( + queryset=LibraryItem.objects.all(), many=True, write_only=True, source="library_items", required=False + ) + comment_count = serializers.IntegerField(source="comments.count", read_only=True) + reaction_count = serializers.IntegerField(source="reactions.count", read_only=True) + viewer_reaction_id = serializers.SerializerMethodField() + + class Meta: + model = Moment + fields = [ + "id", "owner", "caption", "location", "audience", "items", + "library_item_ids", "comment_count", "reaction_count", "viewer_reaction_id", + "created_at", "updated_at", + ] + read_only_fields = [ + "id", "owner", "items", "comment_count", "reaction_count", "viewer_reaction_id", + "created_at", "updated_at", + ] + + def get_viewer_reaction_id(self, obj): + request = self.context.get("request") + if not request or not request.user.is_authenticated: + return None + # obj.reactions is prefetched by the view, so this doesn't re-query per moment. + reaction = next((r for r in obj.reactions.all() if r.user_id == request.user.id), None) + return reaction.id if reaction else None + + def validate_library_item_ids(self, library_items): + request = self.context["request"] + seen_ids = set() + for item in library_items: + if item.owner_id != request.user.id: + raise serializers.ValidationError("Can only share items from your own library.") + if item.id in seen_ids: + raise serializers.ValidationError("The same photo was selected more than once.") + seen_ids.add(item.id) + return library_items + + def validate(self, attrs): + library_items = attrs.get("library_items", []) + caption = attrs.get("caption", "") + if not library_items and not caption.strip(): + raise serializers.ValidationError("A Moment needs a caption or at least one photo.") + return attrs + + def create(self, validated_data): + library_items = validated_data.pop("library_items", []) + validated_data["owner"] = self.context["request"].user + with transaction.atomic(): + moment = Moment.objects.create(**validated_data) + MomentItem.objects.bulk_create( + [MomentItem(moment=moment, library_item=item, order=i) for i, item in enumerate(library_items)] + ) + return moment diff --git a/api/apps/moments/urls.py b/api/apps/moments/urls.py new file mode 100644 index 0000000..58888ca --- /dev/null +++ b/api/apps/moments/urls.py @@ -0,0 +1,19 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter + +from .views import CommentViewSet, MomentViewSet, ReactionViewSet + +router = DefaultRouter() +router.register("", MomentViewSet, basename="moment") + +comments_list = CommentViewSet.as_view({"get": "list", "post": "create"}) +comments_detail = CommentViewSet.as_view({"get": "retrieve", "delete": "destroy"}) +reactions_list = ReactionViewSet.as_view({"get": "list", "post": "create"}) +reactions_detail = ReactionViewSet.as_view({"get": "retrieve", "delete": "destroy"}) + +urlpatterns = [ + path("/comments/", comments_list, name="moment-comments-list"), + path("/comments//", comments_detail, name="moment-comments-detail"), + path("/reactions/", reactions_list, name="moment-reactions-list"), + path("/reactions//", reactions_detail, name="moment-reactions-detail"), +] + router.urls diff --git a/api/apps/moments/views.py b/api/apps/moments/views.py new file mode 100644 index 0000000..ec61009 --- /dev/null +++ b/api/apps/moments/views.py @@ -0,0 +1,69 @@ +from rest_framework import permissions, viewsets +from rest_framework.exceptions import PermissionDenied + +from .models import Comment, Moment, Reaction +from .permissions import IsOwnerOrReadOnly +from .serializers import CommentSerializer, MomentSerializer, ReactionSerializer + + +class MomentViewSet(viewsets.ModelViewSet): + """list() is the home feed: everything visible to the requesting user, + newest first. Filter to one user's posts (for a profile page) with + ?owner__username= — still scoped by visible_to() below, so a + private/followers-only post won't leak through the filter.""" + + serializer_class = MomentSerializer + permission_classes = [permissions.IsAuthenticated, IsOwnerOrReadOnly] + filterset_fields = ["owner__username"] + + def get_queryset(self): + return ( + Moment.objects.visible_to(self.request.user) + .select_related("owner") + .prefetch_related("items__library_item__renditions", "reactions") + ) + + +class CommentViewSet(viewsets.ModelViewSet): + serializer_class = CommentSerializer + permission_classes = [permissions.IsAuthenticated] + http_method_names = ["get", "post", "delete", "head", "options"] + + def _get_moment(self): + return Moment.objects.visible_to(self.request.user).get(pk=self.kwargs["moment_pk"]) + + def get_queryset(self): + return Comment.objects.filter(moment=self._get_moment()).select_related("author") + + def get_serializer_context(self): + context = super().get_serializer_context() + context["moment"] = self._get_moment() + return context + + def perform_create(self, serializer): + serializer.save(moment=self._get_moment(), author=self.request.user) + + def perform_destroy(self, instance): + if instance.author_id != self.request.user.id and instance.moment.owner_id != self.request.user.id: + raise PermissionDenied("Cannot delete another user's comment.") + instance.delete() + + +class ReactionViewSet(viewsets.ModelViewSet): + serializer_class = ReactionSerializer + permission_classes = [permissions.IsAuthenticated] + http_method_names = ["get", "post", "delete", "head", "options"] + + def _get_moment(self): + return Moment.objects.visible_to(self.request.user).get(pk=self.kwargs["moment_pk"]) + + def get_queryset(self): + return Reaction.objects.filter(moment=self._get_moment()).select_related("user") + + def perform_create(self, serializer): + serializer.save(moment=self._get_moment(), user=self.request.user) + + def perform_destroy(self, instance): + if instance.user_id != self.request.user.id: + raise PermissionDenied("Cannot remove another user's reaction.") + instance.delete() diff --git a/api/apps/notifications/__init__.py b/api/apps/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/notifications/admin.py b/api/apps/notifications/admin.py new file mode 100644 index 0000000..4d85afa --- /dev/null +++ b/api/apps/notifications/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from .models import Notification + + +@admin.register(Notification) +class NotificationAdmin(admin.ModelAdmin): + list_display = ["id", "recipient", "actor", "verb", "moment", "is_read", "created_at"] + list_filter = ["verb", "is_read"] diff --git a/api/apps/notifications/apps.py b/api/apps/notifications/apps.py new file mode 100644 index 0000000..6dd96c5 --- /dev/null +++ b/api/apps/notifications/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.notifications" + label = "notifications" + + def ready(self): + from . import signals # noqa: F401 diff --git a/api/apps/notifications/migrations/0001_initial.py b/api/apps/notifications/migrations/0001_initial.py new file mode 100644 index 0000000..2e93340 --- /dev/null +++ b/api/apps/notifications/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.9 on 2026-08-09 21:28 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('moments', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('verb', models.CharField(choices=[('like', 'Like'), ('comment', 'Comment'), ('reply', 'Reply')], max_length=10)), + ('is_read', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)), + ('comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='moments.comment')), + ('moment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='moments.moment')), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['recipient', 'is_read'], name='notificatio_recipie_4e3567_idx')], + }, + ), + ] diff --git a/api/apps/notifications/migrations/__init__.py b/api/apps/notifications/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/apps/notifications/models.py b/api/apps/notifications/models.py new file mode 100644 index 0000000..1e9dad3 --- /dev/null +++ b/api/apps/notifications/models.py @@ -0,0 +1,31 @@ +from django.conf import settings +from django.db import models + + +class Notification(models.Model): + """Tells `recipient` that `actor` did something (`verb`) involving + `moment` (and, for comment/reply verbs, the specific `comment`). + Created by signal handlers in signals.py — never directly by API + consumers.""" + + class Verb(models.TextChoices): + LIKE = "like", "Like" + COMMENT = "comment", "Comment" + REPLY = "reply", "Reply" + + recipient = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="notifications", on_delete=models.CASCADE) + actor = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="+", on_delete=models.CASCADE) + verb = models.CharField(max_length=10, choices=Verb.choices) + moment = models.ForeignKey("moments.Moment", related_name="notifications", on_delete=models.CASCADE) + comment = models.ForeignKey( + "moments.Comment", null=True, blank=True, related_name="notifications", on_delete=models.CASCADE + ) + is_read = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-created_at"] + indexes = [models.Index(fields=["recipient", "is_read"])] + + def __str__(self): + return f"{self.actor} {self.verb} -> {self.recipient}" diff --git a/api/apps/notifications/serializers.py b/api/apps/notifications/serializers.py new file mode 100644 index 0000000..dc14f61 --- /dev/null +++ b/api/apps/notifications/serializers.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + +from apps.accounts.serializers import UserSerializer + +from .models import Notification + + +class NotificationSerializer(serializers.ModelSerializer): + actor = UserSerializer(read_only=True) + moment_id = serializers.IntegerField(read_only=True) + comment_id = serializers.IntegerField(read_only=True) + + class Meta: + model = Notification + fields = ["id", "actor", "verb", "moment_id", "comment_id", "is_read", "created_at"] + read_only_fields = fields diff --git a/api/apps/notifications/signals.py b/api/apps/notifications/signals.py new file mode 100644 index 0000000..d1c1701 --- /dev/null +++ b/api/apps/notifications/signals.py @@ -0,0 +1,49 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from apps.moments.models import Comment, Reaction + +from .models import Notification + + +@receiver(post_save, sender=Reaction) +def notify_on_reaction(sender, instance, created, **kwargs): + if not created: + return + moment = instance.moment + if instance.user_id == moment.owner_id: + return + Notification.objects.create( + recipient_id=moment.owner_id, + actor=instance.user, + verb=Notification.Verb.LIKE, + moment=moment, + ) + + +@receiver(post_save, sender=Comment) +def notify_on_comment(sender, instance, created, **kwargs): + if not created: + return + moment = instance.moment + + if instance.parent_id is None: + if instance.author_id != moment.owner_id: + Notification.objects.create( + recipient_id=moment.owner_id, + actor=instance.author, + verb=Notification.Verb.COMMENT, + moment=moment, + comment=instance, + ) + return + + parent_author_id = instance.parent.author_id + if instance.author_id != parent_author_id: + Notification.objects.create( + recipient_id=parent_author_id, + actor=instance.author, + verb=Notification.Verb.REPLY, + moment=moment, + comment=instance, + ) diff --git a/api/apps/notifications/urls.py b/api/apps/notifications/urls.py new file mode 100644 index 0000000..6f3b42e --- /dev/null +++ b/api/apps/notifications/urls.py @@ -0,0 +1,8 @@ +from rest_framework.routers import DefaultRouter + +from .views import NotificationViewSet + +router = DefaultRouter() +router.register("", NotificationViewSet, basename="notification") + +urlpatterns = router.urls diff --git a/api/apps/notifications/views.py b/api/apps/notifications/views.py new file mode 100644 index 0000000..14a31a8 --- /dev/null +++ b/api/apps/notifications/views.py @@ -0,0 +1,24 @@ +from rest_framework import mixins, permissions, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from .models import Notification +from .serializers import NotificationSerializer + + +class NotificationViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + serializer_class = NotificationSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Notification.objects.filter(recipient=self.request.user).select_related("actor", "moment") + + @action(detail=False, methods=["get"], url_path="unread-count") + def unread_count(self, request): + count = self.get_queryset().filter(is_read=False).count() + return Response({"unread_count": count}) + + @action(detail=False, methods=["post"], url_path="mark-read") + def mark_read(self, request): + self.get_queryset().filter(is_read=False).update(is_read=True) + return Response(status=204) diff --git a/api/config/__init__.py b/api/config/__init__.py new file mode 100644 index 0000000..53f4ccb --- /dev/null +++ b/api/config/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/api/config/asgi.py b/api/config/asgi.py new file mode 100644 index 0000000..856079b --- /dev/null +++ b/api/config/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_asgi_application() diff --git a/api/config/celery.py b/api/config/celery.py new file mode 100644 index 0000000..bd289b2 --- /dev/null +++ b/api/config/celery.py @@ -0,0 +1,9 @@ +import os + +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +app = Celery("aurora_moments") +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() diff --git a/api/config/settings.py b/api/config/settings.py new file mode 100644 index 0000000..37d2eb8 --- /dev/null +++ b/api/config/settings.py @@ -0,0 +1,146 @@ +from datetime import timedelta +from pathlib import Path + +import environ + +BASE_DIR = Path(__file__).resolve().parent.parent + +env = environ.Env( + DEBUG=(bool, False), +) +environ.Env.read_env(BASE_DIR / ".env") + +SECRET_KEY = env("DJANGO_SECRET_KEY", default="insecure-dev-key-change-me") +DEBUG = env("DEBUG") +ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"]) + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "rest_framework_simplejwt", + "corsheaders", + "django_filters", + "apps.core", + "apps.accounts", + "apps.library", + "apps.moments", + "apps.notifications", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" +ASGI_APPLICATION = "config.asgi.application" + +DATABASES = { + "default": env.db("DATABASE_URL", default="postgres://aurora:aurora@postgres:5432/aurora_moments"), +} + +AUTH_USER_MODEL = "accounts.User" + +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, +] + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" + +# --- Media storage --------------------------------------------------------- +# Default: local filesystem, mounted as a volume so it can point at a NAS +# path in self-hosted deployments. Set MEDIA_BACKEND=s3 to switch to any +# S3-compatible object store (AWS S3, MinIO, Backblaze B2, etc.) without +# code changes. +MEDIA_BACKEND = env("MEDIA_BACKEND", default="filesystem") + +if MEDIA_BACKEND == "s3": + STORAGES = { + "default": {"BACKEND": "storages.backends.s3.S3Storage"}, + "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}, + } + AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default="") + AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", default="") + AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME", default="aurora-moments") + AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", default=None) + AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default="us-east-1") + AWS_DEFAULT_ACL = None + AWS_QUERYSTRING_AUTH = False +else: + STORAGES = { + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"}, + } + MEDIA_URL = "media/" + MEDIA_ROOT = env("MEDIA_ROOT", default=str(BASE_DIR / "media")) + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# --- DRF / auth -------------------------------------------------------- +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticated", + ), + "DEFAULT_FILTER_BACKENDS": ( + "django_filters.rest_framework.DjangoFilterBackend", + ), + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 30, +} + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30), + "REFRESH_TOKEN_LIFETIME": timedelta(days=14), + "ROTATE_REFRESH_TOKENS": True, +} + +CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS", default=["http://localhost:5173"]) + +# --- Celery -------------------------------------------------------------- +CELERY_BROKER_URL = env("REDIS_URL", default="redis://redis:6379/0") +CELERY_RESULT_BACKEND = env("REDIS_URL", default="redis://redis:6379/0") +CELERY_ACCEPT_CONTENT = ["json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" +CELERY_TIMEZONE = TIME_ZONE diff --git a/api/config/urls.py b/api/config/urls.py new file mode 100644 index 0000000..8e99b7c --- /dev/null +++ b/api/config/urls.py @@ -0,0 +1,18 @@ +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api/auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/accounts/", include("apps.accounts.urls")), + path("api/library/", include("apps.library.urls")), + path("api/moments/", include("apps.moments.urls")), + path("api/notifications/", include("apps.notifications.urls")), +] + +if settings.DEBUG and settings.MEDIA_BACKEND == "filesystem": + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/api/config/wsgi.py b/api/config/wsgi.py new file mode 100644 index 0000000..8509335 --- /dev/null +++ b/api/config/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/api/manage.py b/api/manage.py new file mode 100644 index 0000000..3e8774a --- /dev/null +++ b/api/manage.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +import os +import sys + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..e5f2286 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,13 @@ +Django==5.0.9 +djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 +django-cors-headers==4.4.0 +django-environ==0.11.2 +django-filter==24.3 +psycopg[binary]==3.2.3 +celery==5.4.0 +redis==5.0.8 +Pillow==10.4.0 +django-storages==1.14.4 +boto3==1.35.36 +gunicorn==23.0.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4a61349 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-aurora_moments} + POSTGRES_USER: ${POSTGRES_USER:-aurora} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-aurora} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-aurora}"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + ports: + - "6380:6379" + + api: + build: ./api + command: sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000" + env_file: .env + volumes: + - ./api:/app + - media_data:/app/media + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + + worker: + build: ./api + command: celery -A config worker --loglevel=info + env_file: .env + volumes: + - ./api:/app + - media_data:/app/media + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + + web: + build: ./web + env_file: .env + environment: + - VITE_API_BASE_URL=${VITE_API_BASE_URL:-http://localhost:8000} + volumes: + - ./web:/app + - /app/node_modules + ports: + - "5173:5173" + depends_on: + - api + +volumes: + postgres_data: + media_data: diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..e4269e8 --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,10 @@ +# Aurora Moments — Mobile (planned) + +Not started yet. Per the V1 plan, the responsive web app plus the REST API +are the priority; the React Native app (iOS first, then Android) starts +once the API in [`../api`](../api) is stable, since the app is mainly a +client for automatic photo backup and the two navigation surfaces +(Library / Home feed). + +When this starts, it should reuse the same `/api/*` endpoints as the web +app (see `api/apps/*/urls.py`) rather than growing a parallel API. diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..9c97bbd --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +.env diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/web/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..d2f0ec3 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm install + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host"] diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/web/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e4559ac --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Aurora Moments + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..b389676 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1385 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.1.1" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.77.0.tgz", + "integrity": "sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.77.0.tgz", + "integrity": "sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.77.0.tgz", + "integrity": "sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.77.0.tgz", + "integrity": "sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.77.0.tgz", + "integrity": "sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.77.0.tgz", + "integrity": "sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.77.0.tgz", + "integrity": "sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.77.0.tgz", + "integrity": "sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.77.0.tgz", + "integrity": "sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.77.0.tgz", + "integrity": "sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.77.0.tgz", + "integrity": "sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.77.0.tgz", + "integrity": "sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.77.0.tgz", + "integrity": "sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.77.0.tgz", + "integrity": "sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.77.0.tgz", + "integrity": "sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.77.0.tgz", + "integrity": "sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.77.0.tgz", + "integrity": "sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.77.0.tgz", + "integrity": "sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.77.0.tgz", + "integrity": "sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.77.0.tgz", + "integrity": "sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.77.0", + "@oxlint/binding-android-arm64": "1.77.0", + "@oxlint/binding-darwin-arm64": "1.77.0", + "@oxlint/binding-darwin-x64": "1.77.0", + "@oxlint/binding-freebsd-x64": "1.77.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.77.0", + "@oxlint/binding-linux-arm-musleabihf": "1.77.0", + "@oxlint/binding-linux-arm64-gnu": "1.77.0", + "@oxlint/binding-linux-arm64-musl": "1.77.0", + "@oxlint/binding-linux-ppc64-gnu": "1.77.0", + "@oxlint/binding-linux-riscv64-gnu": "1.77.0", + "@oxlint/binding-linux-riscv64-musl": "1.77.0", + "@oxlint/binding-linux-s390x-gnu": "1.77.0", + "@oxlint/binding-linux-x64-gnu": "1.77.0", + "@oxlint/binding-linux-x64-musl": "1.77.0", + "@oxlint/binding-openharmony-arm64": "1.77.0", + "@oxlint/binding-win32-arm64-msvc": "1.77.0", + "@oxlint/binding-win32-ia32-msvc": "1.77.0", + "@oxlint/binding-win32-x64-msvc": "1.77.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..d453c1d --- /dev/null +++ b/web/package.json @@ -0,0 +1,26 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.1.1" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.4", + "oxlint": "^1.75.0", + "typescript": "~6.0.2", + "vite": "^8.2.0" + } +} diff --git a/web/public/favicon.png b/web/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a3035712ec8d23cc1a584c859ae5be87b6657c5 GIT binary patch literal 6613 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 TNx{DS00000NkvXXu0mjf)93`& literal 0 HcmV?d00001 diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/icons.svg b/web/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/web/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/logo.png b/web/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b124b224c6f018e1d7d5313808de01cec7a8a5ad GIT binary patch literal 144833 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 literal 0 HcmV?d00001 diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..b48b270 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,81 @@ +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' +import { ProtectedRoute } from './components/ProtectedRoute' +import { Sidebar } from './components/Sidebar' +import { AuthProvider } from './context/AuthContext' +import { CreatePage } from './pages/CreatePage' +import { FeedPage } from './pages/FeedPage' +import { LibraryPage } from './pages/LibraryPage' +import { LoginPage } from './pages/LoginPage' +import { NotificationsPage } from './pages/NotificationsPage' +import { ProfilePage } from './pages/ProfilePage' +import { RegisterPage } from './pages/RegisterPage' +import { SettingsPage } from './pages/SettingsPage' + +function App() { + return ( + + +
+ +
+ + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + +
+
+
+
+ ) +} + +export default App diff --git a/web/src/api/accounts.ts b/web/src/api/accounts.ts new file mode 100644 index 0000000..9ccd947 --- /dev/null +++ b/web/src/api/accounts.ts @@ -0,0 +1,22 @@ +import { api } from './client' +import type { Profile, User } from './types' + +export function getProfile(username: string): Promise { + return api.get(`/api/accounts/users/${username}/`) +} + +export function updateProfile(data: { display_name?: string; bio?: string; avatar?: File }): Promise { + const formData = new FormData() + if (data.display_name !== undefined) formData.append('display_name', data.display_name) + if (data.bio !== undefined) formData.append('bio', data.bio) + if (data.avatar) formData.append('avatar', data.avatar) + return api.patch('/api/accounts/me/', formData) +} + +export function followUser(userId: number): Promise<{ id: number }> { + return api.post<{ id: number }>('/api/accounts/follows/', { following_id: userId }) +} + +export function unfollowUser(followId: number): Promise { + return api.delete(`/api/accounts/follows/${followId}/`) +} diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts new file mode 100644 index 0000000..bf69d39 --- /dev/null +++ b/web/src/api/auth.ts @@ -0,0 +1,25 @@ +import { api, tokenStorage } from './client' +import type { User } from './types' + +interface TokenPair { + access: string + refresh: string +} + +export async function login(email: string, password: string): Promise { + const tokens = await api.post('/api/auth/token/', { email, password }) + tokenStorage.set(tokens.access, tokens.refresh) +} + +export async function register(email: string, username: string, password: string): Promise { + await api.post('/api/accounts/register/', { email, username, password }) + await login(email, password) +} + +export function logout(): void { + tokenStorage.clear() +} + +export function getMe(): Promise { + return api.get('/api/accounts/me/') +} diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000..1386da6 --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,99 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000' + +const ACCESS_TOKEN_KEY = 'aurora_access_token' +const REFRESH_TOKEN_KEY = 'aurora_refresh_token' + +export const tokenStorage = { + getAccess: () => localStorage.getItem(ACCESS_TOKEN_KEY), + getRefresh: () => localStorage.getItem(REFRESH_TOKEN_KEY), + set: (access: string, refresh: string) => { + localStorage.setItem(ACCESS_TOKEN_KEY, access) + localStorage.setItem(REFRESH_TOKEN_KEY, refresh) + }, + clear: () => { + localStorage.removeItem(ACCESS_TOKEN_KEY) + localStorage.removeItem(REFRESH_TOKEN_KEY) + }, +} + +export class ApiError extends Error { + status: number + body: unknown + + constructor(status: number, body: unknown) { + super(`API error ${status}`) + this.status = status + this.body = body + } +} + +// Called when a refresh attempt fails outright (refresh token missing, +// expired, or revoked) so the app can drop back to a logged-out state. +let onAuthFailure: (() => void) | null = null + +export function setAuthFailureHandler(handler: () => void): void { + onAuthFailure = handler +} + +// Access tokens are short-lived (30 min, see SIMPLE_JWT in api/config/settings.py). +// Concurrent requests that all hit a 401 at once should trigger a single +// refresh call, not one each — this promise is shared across them. +let refreshPromise: Promise | null = null + +async function refreshAccessToken(): Promise { + const refresh = tokenStorage.getRefresh() + if (!refresh) return null + + if (!refreshPromise) { + refreshPromise = fetch(`${API_BASE_URL}/api/auth/token/refresh/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh }), + }) + .then(async (response) => { + if (!response.ok) return null + const body = await response.json() + tokenStorage.set(body.access, body.refresh ?? refresh) + return body.access as string + }) + .catch(() => null) + .finally(() => { + refreshPromise = null + }) + } + + return refreshPromise +} + +async function request(path: string, options: RequestInit = {}, isRetry = false): Promise { + const access = tokenStorage.getAccess() + const headers = new Headers(options.headers) + if (access) headers.set('Authorization', `Bearer ${access}`) + const isFormData = options.body instanceof FormData + if (!isFormData && options.body) headers.set('Content-Type', 'application/json') + + const response = await fetch(`${API_BASE_URL}${path}`, { ...options, headers }) + + if (response.status === 401 && !isRetry) { + const newAccess = await refreshAccessToken() + if (newAccess) return request(path, options, true) + + tokenStorage.clear() + onAuthFailure?.() + } + + if (response.status === 204) return undefined as T + + const body = await response.json().catch(() => undefined) + if (!response.ok) throw new ApiError(response.status, body) + return body as T +} + +export const api = { + get: (path: string) => request(path), + post: (path: string, data?: unknown) => + request(path, { method: 'POST', body: data instanceof FormData ? data : JSON.stringify(data) }), + patch: (path: string, data?: unknown) => + request(path, { method: 'PATCH', body: data instanceof FormData ? data : JSON.stringify(data) }), + delete: (path: string) => request(path, { method: 'DELETE' }), +} diff --git a/web/src/api/library.ts b/web/src/api/library.ts new file mode 100644 index 0000000..5cbfeb6 --- /dev/null +++ b/web/src/api/library.ts @@ -0,0 +1,14 @@ +import { api } from './client' +import type { LibraryItem, Paginated } from './types' + +export function listLibraryItems(): Promise> { + return api.get>('/api/library/items/') +} + +export function uploadLibraryItem(file: File): Promise { + const formData = new FormData() + formData.append('file', file) + formData.append('media_type', file.type.startsWith('video') ? 'video' : 'photo') + formData.append('original_filename', file.name) + return api.post('/api/library/items/', formData) +} diff --git a/web/src/api/moments.ts b/web/src/api/moments.ts new file mode 100644 index 0000000..6c45b57 --- /dev/null +++ b/web/src/api/moments.ts @@ -0,0 +1,47 @@ +import { api } from './client' +import type { Audience, Comment, Moment, Paginated, Reaction } from './types' + +export function listFeed(): Promise> { + return api.get>('/api/moments/') +} + +export function listUserMoments(username: string): Promise> { + return api.get>(`/api/moments/?owner__username=${encodeURIComponent(username)}`) +} + +export function shareMoment(params: { + caption: string + location?: string + audience: Audience + libraryItemIds: string[] +}): Promise { + return api.post('/api/moments/', { + caption: params.caption, + location: params.location ?? '', + audience: params.audience, + library_item_ids: params.libraryItemIds, + }) +} + +export function listComments(momentId: number): Promise> { + return api.get>(`/api/moments/${momentId}/comments/`) +} + +export function addComment(momentId: number, body: string, parentId?: number): Promise { + return api.post(`/api/moments/${momentId}/comments/`, { + body, + ...(parentId ? { parent: parentId } : {}), + }) +} + +export function deleteComment(momentId: number, commentId: number): Promise { + return api.delete(`/api/moments/${momentId}/comments/${commentId}/`) +} + +export function addReaction(momentId: number): Promise { + return api.post(`/api/moments/${momentId}/reactions/`, { reaction_type: 'like' }) +} + +export function removeReaction(momentId: number, reactionId: number): Promise { + return api.delete(`/api/moments/${momentId}/reactions/${reactionId}/`) +} diff --git a/web/src/api/notifications.ts b/web/src/api/notifications.ts new file mode 100644 index 0000000..2e1f7c4 --- /dev/null +++ b/web/src/api/notifications.ts @@ -0,0 +1,14 @@ +import { api } from './client' +import type { AppNotification, Paginated } from './types' + +export function listNotifications(): Promise> { + return api.get>('/api/notifications/') +} + +export function getUnreadCount(): Promise<{ unread_count: number }> { + return api.get<{ unread_count: number }>('/api/notifications/unread-count/') +} + +export function markAllNotificationsRead(): Promise { + return api.post('/api/notifications/mark-read/') +} diff --git a/web/src/api/types.ts b/web/src/api/types.ts new file mode 100644 index 0000000..3a28365 --- /dev/null +++ b/web/src/api/types.ts @@ -0,0 +1,100 @@ +export interface User { + id: number + username: string + display_name: string + bio: string + avatar: string | null + is_private: boolean + date_joined: string +} + +export interface MediaRendition { + kind: 'thumbnail' | 'feed' | 'large' + file: string + width: number + height: number +} + +export interface LibraryItem { + id: string + file: string + media_type: 'photo' | 'video' + original_filename: string + file_size: number + width: number | null + height: number | null + duration_seconds: number | null + captured_at: string | null + camera_make: string + camera_model: string + processing_status: 'pending' | 'processing' | 'ready' | 'failed' + renditions: MediaRendition[] + created_at: string +} + +export interface MomentItem { + id: number + library_item: LibraryItem + order: number +} + +export type Audience = 'public' | 'followers' | 'friends' | 'family' | 'custom' | 'only_me' + +export interface Moment { + id: number + owner: User + caption: string + location: string + audience: Audience + items: MomentItem[] + comment_count: number + reaction_count: number + viewer_reaction_id: number | null + created_at: string + updated_at: string +} + +export interface Comment { + id: number + moment: number + author: User + parent: number | null + body: string + created_at: string +} + +export interface Reaction { + id: number + moment: number + user: User + reaction_type: 'like' + created_at: string +} + +export type FollowStatus = 'pending' | 'accepted' + +export interface Profile extends User { + follower_count: number + following_count: number + viewer_follow_id: number | null + viewer_follow_status: FollowStatus | null +} + +export type NotificationVerb = 'like' | 'comment' | 'reply' + +export interface AppNotification { + id: number + actor: User + verb: NotificationVerb + moment_id: number + comment_id: number | null + is_read: boolean + created_at: string +} + +export interface Paginated { + count: number + next: string | null + previous: string | null + results: T[] +} diff --git a/web/src/assets/vite.svg b/web/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/web/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/web/src/components/Avatar.tsx b/web/src/components/Avatar.tsx new file mode 100644 index 0000000..5b21100 --- /dev/null +++ b/web/src/components/Avatar.tsx @@ -0,0 +1,7 @@ +export function Avatar({ user }: { user: { username: string; avatar: string | null } }) { + return user.avatar ? ( + {user.username} + ) : ( + {user.username.slice(0, 1).toUpperCase()} + ) +} diff --git a/web/src/components/MomentCard.tsx b/web/src/components/MomentCard.tsx new file mode 100644 index 0000000..7a1f01d --- /dev/null +++ b/web/src/components/MomentCard.tsx @@ -0,0 +1,249 @@ +import { useState } from 'react' +import type { FormEvent } from 'react' +import { Link } from 'react-router-dom' +import { addComment, addReaction, listComments, removeReaction } from '../api/moments' +import type { Comment, Moment, MomentItem } from '../api/types' +import { Avatar } from './Avatar' +import { formatRelativeTime } from '../utils/formatRelativeTime' + +function itemImageUrl(item: MomentItem): string { + const rendition = item.library_item.renditions.find((r) => r.kind === 'feed') + return rendition?.file ?? item.library_item.file +} + +function itemPosterUrl(item: MomentItem): string | undefined { + return item.library_item.renditions.find((r) => r.kind === 'feed')?.file +} + +function MomentCarousel({ moment }: { moment: Moment }) { + const [index, setIndex] = useState(0) + const items = moment.items + + if (items.length === 0) return null + + const goTo = (next: number) => setIndex((next + items.length) % items.length) + const current = items[index] + + return ( +
+ {current.library_item.media_type === 'video' ? ( +
+ ) +} + +export function MomentCard({ moment }: { moment: Moment }) { + const [reactionId, setReactionId] = useState(moment.viewer_reaction_id) + const [reactionCount, setReactionCount] = useState(moment.reaction_count) + const [reacting, setReacting] = useState(false) + + const [commentsOpen, setCommentsOpen] = useState(false) + const [commentsLoaded, setCommentsLoaded] = useState(false) + const [comments, setComments] = useState([]) + const [commentCount, setCommentCount] = useState(moment.comment_count) + const [newComment, setNewComment] = useState('') + const [submittingComment, setSubmittingComment] = useState(false) + + const [replyTarget, setReplyTarget] = useState<{ parentId: number; username: string } | null>(null) + const [replyBody, setReplyBody] = useState('') + const [submittingReply, setSubmittingReply] = useState(false) + + const liked = reactionId !== null + + const toggleLike = async () => { + if (reacting) return + setReacting(true) + try { + if (liked && reactionId !== null) { + await removeReaction(moment.id, reactionId) + setReactionId(null) + setReactionCount((count) => count - 1) + } else { + const reaction = await addReaction(moment.id) + setReactionId(reaction.id) + setReactionCount((count) => count + 1) + } + } finally { + setReacting(false) + } + } + + const toggleComments = async () => { + const opening = !commentsOpen + setCommentsOpen(opening) + if (opening && !commentsLoaded) { + const page = await listComments(moment.id) + setComments(page.results) + setCommentsLoaded(true) + } + } + + const submitComment = async (event: FormEvent) => { + event.preventDefault() + const body = newComment.trim() + if (!body || submittingComment) return + setSubmittingComment(true) + try { + const comment = await addComment(moment.id, body) + setComments((existing) => [...existing, comment]) + setCommentCount((count) => count + 1) + setNewComment('') + } finally { + setSubmittingComment(false) + } + } + + const startReply = (parentId: number, username: string) => { + setReplyTarget({ parentId, username }) + setReplyBody(`@${username} `) + } + + const cancelReply = () => { + setReplyTarget(null) + setReplyBody('') + } + + const submitReply = async (event: FormEvent) => { + event.preventDefault() + const body = replyBody.trim() + if (!body || !replyTarget || submittingReply) return + setSubmittingReply(true) + try { + const reply = await addComment(moment.id, body, replyTarget.parentId) + setComments((existing) => [...existing, reply]) + setCommentCount((count) => count + 1) + cancelReply() + } finally { + setSubmittingReply(false) + } + } + + const topLevelComments = comments.filter((comment) => comment.parent === null) + const repliesByParent = new Map() + for (const comment of comments) { + if (comment.parent === null) continue + const existing = repliesByParent.get(comment.parent) ?? [] + existing.push(comment) + repliesByParent.set(comment.parent, existing) + } + + return ( +
+
+ + @{moment.owner.username} + + {formatRelativeTime(moment.created_at)} +
+ + {moment.caption &&

{moment.caption}

} +
+ + +
+ {commentsOpen && ( +
+ {topLevelComments.map((comment) => ( +
+

+ + @{comment.author.username} + {' '} + {comment.body} + +

+ {(repliesByParent.get(comment.id) ?? []).map((reply) => ( +

+ + @{reply.author.username} + {' '} + {reply.body} + +

+ ))} + {replyTarget?.parentId === comment.id && ( +
+ setReplyBody(e.target.value)} + disabled={submittingReply} + autoFocus + /> + + +
+ )} +
+ ))} + {comments.length === 0 &&

No comments yet.

} +
+ setNewComment(e.target.value)} + disabled={submittingComment} + /> + +
+
+ )} +
+ ) +} diff --git a/web/src/components/ProtectedRoute.tsx b/web/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..3fdcebf --- /dev/null +++ b/web/src/components/ProtectedRoute.tsx @@ -0,0 +1,11 @@ +import type { ReactNode } from 'react' +import { Navigate } from 'react-router-dom' +import { useAuth } from '../context/AuthContext' + +export function ProtectedRoute({ children }: { children: ReactNode }) { + const { user, loading } = useAuth() + + if (loading) return
Loading…
+ if (!user) return + return <>{children} +} diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx new file mode 100644 index 0000000..3d52f8e --- /dev/null +++ b/web/src/components/Sidebar.tsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react' +import { Link, NavLink, useNavigate } from 'react-router-dom' +import { getUnreadCount } from '../api/notifications' +import { useAuth } from '../context/AuthContext' +import { Avatar } from './Avatar' +import { CreateIcon, HomeIcon, LibraryIcon, LogoutIcon, NotificationsIcon } from './icons' + +const linkClassName = ({ isActive }: { isActive: boolean }) => + isActive ? 'sidebar-link sidebar-link-active' : 'sidebar-link' + +export function Sidebar() { + const { user, logout } = useAuth() + const navigate = useNavigate() + const [unreadCount, setUnreadCount] = useState(0) + + useEffect(() => { + if (!user) return + let cancelled = false + const poll = () => { + getUnreadCount() + .then(({ unread_count }) => { + if (!cancelled) setUnreadCount(unread_count) + }) + .catch(() => {}) + } + poll() + const interval = setInterval(poll, 15000) + return () => { + cancelled = true + clearInterval(interval) + } + }, [user]) + + if (!user) return null + + return ( + + ) +} diff --git a/web/src/components/icons.tsx b/web/src/components/icons.tsx new file mode 100644 index 0000000..4ceb35f --- /dev/null +++ b/web/src/components/icons.tsx @@ -0,0 +1,59 @@ +type IconProps = { className?: string } + +export function HomeIcon({ className }: IconProps) { + return ( + + + + ) +} + +export function CreateIcon({ className }: IconProps) { + return ( + + + + + ) +} + +export function LibraryIcon({ className }: IconProps) { + return ( + + + + + + ) +} + +export function NotificationsIcon({ className }: IconProps) { + return ( + + + + ) +} + +export function VideoIcon({ className }: IconProps) { + return ( + + + + + ) +} + +export function LogoutIcon({ className }: IconProps) { + return ( + + + + + + ) +} diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx new file mode 100644 index 0000000..8299746 --- /dev/null +++ b/web/src/context/AuthContext.tsx @@ -0,0 +1,77 @@ +import { createContext, useCallback, useContext, useEffect, useState } from 'react' +import type { ReactNode } from 'react' +import * as authApi from '../api/auth' +import { setAuthFailureHandler, tokenStorage } from '../api/client' +import type { User } from '../api/types' + +interface AuthContextValue { + user: User | null + loading: boolean + login: (email: string, password: string) => Promise + register: (email: string, username: string, password: string) => Promise + logout: () => void + refreshUser: () => Promise +} + +const AuthContext = createContext(undefined) + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + const refreshMe = useCallback(async () => { + if (!tokenStorage.getAccess()) { + setUser(null) + setLoading(false) + return + } + try { + setUser(await authApi.getMe()) + } catch { + tokenStorage.clear() + setUser(null) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + refreshMe() + }, [refreshMe]) + + // Fires when a request's 401 survives a refresh attempt (refresh token + // missing/expired/revoked) — drop back to logged-out state so + // ProtectedRoute sends the user to /login instead of the app silently + // failing every request. + useEffect(() => { + setAuthFailureHandler(() => setUser(null)) + return () => setAuthFailureHandler(() => {}) + }, []) + + const login = async (email: string, password: string) => { + await authApi.login(email, password) + await refreshMe() + } + + const register = async (email: string, username: string, password: string) => { + await authApi.register(email, username, password) + await refreshMe() + } + + const logout = () => { + authApi.logout() + setUser(null) + } + + return ( + + {children} + + ) +} + +export function useAuth(): AuthContextValue { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used within AuthProvider') + return ctx +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..7601537 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,766 @@ +:root { + color-scheme: dark; + --aurora-bg: #0b0d12; + --aurora-surface: #14161d; + --aurora-border: #262a35; + --aurora-text: #e8e9ee; + --aurora-text-dim: #9199a8; + --aurora-accent: #7c5cff; + --aurora-accent-2: #ff5c9e; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--aurora-bg); + color: var(--aurora-text); + min-height: 100vh; +} + +a { + color: var(--aurora-accent); + text-decoration: none; +} + +button { + background: linear-gradient(135deg, var(--aurora-accent), var(--aurora-accent-2)); + color: white; + border: none; + border-radius: 8px; + padding: 0.6rem 1.2rem; + font-weight: 600; + cursor: pointer; +} + +button:disabled { + opacity: 0.6; + cursor: default; +} + +input, +textarea { + background: var(--aurora-surface); + border: 1px solid var(--aurora-border); + border-radius: 8px; + padding: 0.6rem 0.8rem; + color: var(--aurora-text); + font-size: 1rem; + font-family: inherit; +} + +.app-shell { + min-height: 100vh; +} + +.sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 88px; + display: flex; + flex-direction: column; + padding: 1.5rem 0 1.5rem 1.75rem; + background: var(--aurora-bg); + overflow: hidden; + white-space: nowrap; + transition: width 0.2s ease; + z-index: 100; +} + +.sidebar:hover { + width: 260px; +} + +.sidebar-brand { + display: flex; + align-items: center; + gap: 0.9rem; + font-weight: 700; + font-size: 1.1rem; + margin-bottom: 2rem; + padding: 0 0.75rem; +} + +.sidebar-logo { + width: 32px; + height: 32px; + flex-shrink: 0; +} + +.sidebar-brand-full { + background: linear-gradient(135deg, var(--aurora-accent), var(--aurora-accent-2)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + opacity: 0; + transform: translateX(-8px); + transition: opacity 0.15s ease, transform 0.15s ease; +} + +.sidebar:hover .sidebar-brand-full { + opacity: 1; + transform: translateX(0); +} + +.sidebar-links-center { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; + justify-content: center; +} + +.sidebar-bottom { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.sidebar-link { + display: flex; + align-items: center; + gap: 0.9rem; + padding: 0.75rem; + border-radius: 10px; + background: transparent; + border: none; + color: var(--aurora-text); + font-weight: 500; + font-size: 1rem; + font-family: inherit; + text-align: left; + cursor: pointer; +} + +.sidebar-link:hover { + background: var(--aurora-surface); +} + +.sidebar-link-active { + background: var(--aurora-surface); + font-weight: 700; +} + +.sidebar-link svg { + width: 24px; + height: 24px; + flex-shrink: 0; +} + +.sidebar-link span:not(.sidebar-icon-wrap):not(.sidebar-badge) { + opacity: 0; + transform: translateX(-8px); + transition: opacity 0.15s ease, transform 0.15s ease; +} + +.sidebar:hover .sidebar-link span:not(.sidebar-icon-wrap):not(.sidebar-badge) { + opacity: 1; + transform: translateX(0); +} + +.sidebar-icon-wrap { + position: relative; + display: inline-flex; +} + +.sidebar-badge { + position: absolute; + top: -4px; + right: -6px; + background: var(--aurora-accent-2); + color: white; + font-size: 0.65rem; + font-weight: 700; + border-radius: 999px; + min-width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 3px; + line-height: 1; +} + +.avatar { + width: 24px; + height: 24px; + border-radius: 50%; + flex-shrink: 0; + object-fit: cover; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--aurora-accent), var(--aurora-accent-2)); + color: white; + font-size: 0.7rem; + font-weight: 700; +} + +.app-main { + min-width: 0; + max-width: 720px; + margin-left: max(120px, calc(50% - 360px)); + margin-right: auto; + padding: 1.5rem; +} + +.create-panel { + margin: 0 auto; +} + +.page-loading, +.feed-empty { + text-align: center; + color: var(--aurora-text-dim); + padding: 4rem 1rem; +} + +.notifications-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; +} + +.notification-row-unread { + background: var(--aurora-surface); + border-radius: 10px; +} + +.notification-link { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + color: var(--aurora-text); +} + +.notification-link .avatar { + width: 36px; + height: 36px; +} + +.notification-text { + flex: 1; +} + +.notification-time { + color: var(--aurora-text-dim); + font-size: 0.8rem; + flex-shrink: 0; +} + +.auth-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 0.9rem; + width: 100%; + max-width: 360px; + padding: 2rem; + background: var(--aurora-surface); + border: 1px solid var(--aurora-border); + border-radius: 16px; +} + +.auth-form h1 { + margin: 0; + background: linear-gradient(135deg, var(--aurora-accent), var(--aurora-accent-2)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.auth-tagline { + margin: 0 0 0.5rem; + color: var(--aurora-text-dim); +} + +.auth-error { + color: var(--aurora-accent-2); + margin: 0; +} + +.library-header { + margin-bottom: 1.5rem; +} + +.library-subtitle { + color: var(--aurora-text-dim); +} + +.library-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 0.5rem; +} + +.library-tile { + position: relative; + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; + background: var(--aurora-surface); + cursor: pointer; + border: 2px solid transparent; +} + +.grid-video-badge { + position: absolute; + top: 6px; + right: 6px; + width: 18px; + height: 18px; + color: white; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8)); +} + +.library-tile-selected { + border-color: var(--aurora-accent); +} + +.library-tile img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.library-tile-pending { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--aurora-text-dim); + font-size: 0.8rem; +} + +.moment-card { + background: var(--aurora-surface); + border: 1px solid var(--aurora-border); + border-radius: 16px; + overflow: hidden; + margin-bottom: 1.5rem; +} + +.moment-card header { + display: flex; + justify-content: space-between; + padding: 0.9rem 1rem; + color: var(--aurora-text-dim); +} + +.moment-author { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.moment-carousel { + position: relative; +} + +.moment-carousel img, +.moment-carousel video { + width: 100%; + display: block; + background: #000; +} + +.carousel-arrow { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(11, 13, 18, 0.6); + color: var(--aurora-text); + border: none; + border-radius: 999px; + width: 32px; + height: 32px; + font-size: 1.2rem; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.carousel-arrow-prev { + left: 0.6rem; +} + +.carousel-arrow-next { + right: 0.6rem; +} + +.carousel-dots { + position: absolute; + bottom: 0.7rem; + left: 0; + right: 0; + display: flex; + justify-content: center; + gap: 0.35rem; +} + +.carousel-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.4); +} + +.carousel-dot-active { + background: var(--aurora-text); +} + +.moment-caption { + padding: 0.9rem 1rem 0; + margin: 0; +} + +.moment-card footer { + display: flex; + gap: 0.75rem; + padding: 0.9rem 1rem; +} + +.reaction-button { + display: flex; + align-items: center; + gap: 0.35rem; + background: transparent; + border: 1px solid var(--aurora-border); + border-radius: 999px; + padding: 0.35rem 0.8rem; + color: var(--aurora-text-dim); + font-weight: 500; +} + +.reaction-button-active { + color: var(--aurora-accent-2); + border-color: var(--aurora-accent-2); +} + +.moment-comments { + border-top: 1px solid var(--aurora-border); + padding: 0.9rem 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.moment-comment-thread { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.moment-comment { + margin: 0; + font-size: 0.9rem; +} + +.moment-comment-reply { + margin-left: 2rem; +} + +.moment-comment-reply-link { + background: none; + border: none; + color: var(--aurora-text-dim); + font-size: 0.8rem; + font-weight: 600; + font-family: inherit; + cursor: pointer; + padding: 0; + margin-left: 0.5rem; +} + +.moment-comment-reply-link:hover { + color: var(--aurora-text); +} + +.moment-reply-form { + margin-left: 2rem; +} + +.moment-comment-author { + display: inline-flex; + align-items: center; + gap: 0.35rem; + color: var(--aurora-accent); + font-weight: 600; + vertical-align: middle; +} + +.moment-comments-empty { + margin: 0; + color: var(--aurora-text-dim); + font-size: 0.85rem; +} + +.moment-comment-form { + display: flex; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.moment-comment-form input { + flex: 1; +} + +.selection-bar { + position: sticky; + bottom: 1rem; + margin-top: 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + background: var(--aurora-surface); + border: 1px solid var(--aurora-border); + border-radius: 12px; + padding: 0.9rem 1.2rem; +} + +.selection-bar div { + display: flex; + gap: 0.6rem; +} + +.composer-cancel { + background: transparent; + border: 1px solid var(--aurora-border); + color: var(--aurora-text); +} + +.composer-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 10; +} + +.composer-panel { + display: flex; + flex-direction: column; + gap: 0.8rem; + width: 100%; + max-width: 480px; + max-height: 90vh; + overflow-y: auto; + background: var(--aurora-surface); + border: 1px solid var(--aurora-border); + border-radius: 16px; + padding: 1.5rem; +} + +.composer-panel h2 { + margin: 0; +} + +.composer-thumbs { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.composer-thumb { + position: relative; + width: 64px; + height: 64px; +} + +.composer-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; +} + +.composer-panel textarea { + background: var(--aurora-bg); + border: 1px solid var(--aurora-border); + border-radius: 8px; + padding: 0.6rem 0.8rem; + color: var(--aurora-text); + font-family: inherit; + font-size: 1rem; + resize: vertical; +} + +.composer-audience-label { + display: flex; + flex-direction: column; + gap: 0.35rem; + color: var(--aurora-text-dim); + font-size: 0.9rem; +} + +.composer-audience-label select { + background: var(--aurora-bg); + border: 1px solid var(--aurora-border); + border-radius: 8px; + padding: 0.5rem 0.7rem; + color: var(--aurora-text); + font-size: 1rem; +} + +.composer-actions { + display: flex; + justify-content: flex-end; + gap: 0.6rem; +} + +.profile-header { + display: flex; + gap: 1.5rem; + align-items: flex-start; + margin-bottom: 2rem; +} + +.profile-avatar { + flex-shrink: 0; + width: 84px; + height: 84px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + font-weight: 700; + color: white; + background: linear-gradient(135deg, var(--aurora-accent), var(--aurora-accent-2)); +} + +.profile-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profile-info { + flex: 1; + min-width: 0; +} + +.profile-name-row { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.profile-name-row h1 { + margin: 0; + font-size: 1.3rem; +} + +.profile-bio { + margin: 0.2rem 0 0; + color: var(--aurora-text-dim); + white-space: pre-wrap; +} + +.profile-counts { + display: flex; + gap: 1.5rem; + margin-top: 0.9rem; + color: var(--aurora-text-dim); +} + +.profile-counts strong { + color: var(--aurora-text); +} + +.profile-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.35rem; +} + +.profile-grid-tile { + position: relative; + aspect-ratio: 1; + padding: 0; + border: none; + border-radius: 4px; + overflow: hidden; + background: var(--aurora-surface); + cursor: pointer; +} + +.profile-grid-tile img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.profile-modal { + width: 100%; + max-width: 480px; + max-height: 90vh; + overflow-y: auto; + border-radius: 16px; +} + +.settings-avatar { + position: relative; + align-self: center; + width: 84px; + height: 84px; + border-radius: 50%; + overflow: hidden; + padding: 0; + border: none; + background: linear-gradient(135deg, var(--aurora-accent), var(--aurora-accent-2)); + color: white; + font-size: 2rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.settings-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.settings-avatar-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.55); + color: white; + font-size: 0.6rem; + font-weight: 600; + text-align: center; + opacity: 0; + transition: opacity 0.15s; +} + +.settings-avatar:hover .settings-avatar-overlay { + opacity: 1; +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/web/src/pages/CreatePage.tsx b/web/src/pages/CreatePage.tsx new file mode 100644 index 0000000..58fe846 --- /dev/null +++ b/web/src/pages/CreatePage.tsx @@ -0,0 +1,158 @@ +import { useState } from 'react' +import type { FormEvent } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { listLibraryItems } from '../api/library' +import { shareMoment } from '../api/moments' +import type { Audience, LibraryItem } from '../api/types' +import { VideoIcon } from '../components/icons' + +const AUDIENCE_OPTIONS: { value: Audience; label: string }[] = [ + { value: 'public', label: 'Public' }, + { value: 'followers', label: 'Followers' }, + { value: 'friends', label: 'Friends' }, + { value: 'family', label: 'Family' }, + { value: 'only_me', label: 'Only me' }, +] + +function thumbnailUrl(item: LibraryItem): string { + const thumb = item.renditions.find((r) => r.kind === 'thumbnail') + return thumb?.file ?? item.file +} + +interface CreatePageState { + preselectedItems?: LibraryItem[] +} + +export function CreatePage() { + const location = useLocation() + const navigate = useNavigate() + const preselected = (location.state as CreatePageState | null)?.preselectedItems ?? [] + + const [selectedIds, setSelectedIds] = useState>(new Set(preselected.map((item) => item.id))) + const [selectedItems, setSelectedItems] = useState(preselected) + + const [pickerOpen, setPickerOpen] = useState(false) + const [pickerItems, setPickerItems] = useState([]) + const [pickerLoaded, setPickerLoaded] = useState(false) + + const [caption, setCaption] = useState('') + const [locationText, setLocationText] = useState('') + const [audience, setAudience] = useState('public') + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + const openPicker = async () => { + setPickerOpen((open) => !open) + if (!pickerLoaded) { + const page = await listLibraryItems() + setPickerItems(page.results.filter((item) => item.processing_status === 'ready')) + setPickerLoaded(true) + } + } + + const toggleItem = (item: LibraryItem) => { + setSelectedIds((existing) => { + const next = new Set(existing) + if (next.has(item.id)) next.delete(item.id) + else next.add(item.id) + return next + }) + setSelectedItems((items) => { + const alreadySelected = items.some((existingItem) => existingItem.id === item.id) + return alreadySelected ? items.filter((existingItem) => existingItem.id !== item.id) : [...items, item] + }) + } + + const canSubmit = caption.trim().length > 0 || selectedItems.length > 0 + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault() + if (!canSubmit || submitting) return + setSubmitting(true) + setError(null) + try { + await shareMoment({ + caption, + location: locationText, + audience, + libraryItemIds: selectedItems.map((item) => item.id), + }) + navigate('/') + } catch { + setError('Could not share this Moment. Try again.') + } finally { + setSubmitting(false) + } + } + + return ( +
+

Create a Moment

+ {selectedItems.length > 0 && ( +
+ {selectedItems.map((item) => ( +
+ {item.original_filename} + {item.media_type === 'video' && } +
+ ))} +
+ )} +