diff --git a/backend/apps/comment/__init__.py b/backend/apps/comment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/comment/admin.py b/backend/apps/comment/admin.py new file mode 100644 index 0000000..e3c9d31 --- /dev/null +++ b/backend/apps/comment/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from .models import Comment + +# Register your models here. + + +@admin.register(Comment) +class CommentAdmin(admin.ModelAdmin): + list_display = ('quibbler', 'content', 'created_at') + search_fields = ('quibbler__username', 'content') diff --git a/backend/apps/comment/api/__init__.py b/backend/apps/comment/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/comment/api/v1/__init__.py b/backend/apps/comment/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/comment/api/v1/serializers.py b/backend/apps/comment/api/v1/serializers.py new file mode 100644 index 0000000..99dbd2b --- /dev/null +++ b/backend/apps/comment/api/v1/serializers.py @@ -0,0 +1,30 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import serializers + +from ...models import Comment + + +class CommentSerializer(serializers.ModelSerializer): + path = serializers.CharField(required=False) + + class Meta: + model = Comment + fields = '__all__' + + def create(self, validated_data): + data = { + 'quibbler': validated_data.get('quibbler') + or self.context['request'].user_profile, + 'content': validated_data['content'], + } + + if path := validated_data.get('path'): + parent_instance = get_object_or_404(Comment, path__match=path) + comment_instance: Comment = Comment.objects.create_child( + parent=parent_instance, **data + ) + else: + comment_instance: Comment = Comment.objects.create_child(**data) + + comment_instance.save() + return comment_instance diff --git a/backend/apps/comment/api/v1/urls.py b/backend/apps/comment/api/v1/urls.py new file mode 100644 index 0000000..4a4b80b --- /dev/null +++ b/backend/apps/comment/api/v1/urls.py @@ -0,0 +1,8 @@ +from rest_framework import routers + +from .viewsets import CommentViewSet + +router = routers.DefaultRouter() +router.register(r'', CommentViewSet) + +urlpatterns = router.urls diff --git a/backend/apps/comment/api/v1/viewsets.py b/backend/apps/comment/api/v1/viewsets.py new file mode 100644 index 0000000..ca2b9a7 --- /dev/null +++ b/backend/apps/comment/api/v1/viewsets.py @@ -0,0 +1,9 @@ +from rest_framework import viewsets + +from ...models import Comment +from .serializers import CommentSerializer + + +class CommentViewSet(viewsets.ModelViewSet): + queryset = Comment.objects.all() + serializer_class = CommentSerializer diff --git a/backend/apps/comment/apps.py b/backend/apps/comment/apps.py new file mode 100644 index 0000000..1b4ed7f --- /dev/null +++ b/backend/apps/comment/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CommentConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.comment' diff --git a/backend/apps/comment/migrations/0001_initial.py b/backend/apps/comment/migrations/0001_initial.py new file mode 100644 index 0000000..d3fed2d --- /dev/null +++ b/backend/apps/comment/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.4 on 2024-12-13 12:02 + +import django_ltree.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('path', django_ltree.fields.PathField(unique=True)), + ( + 'created_at', + models.DateTimeField(auto_now_add=True, verbose_name='create at'), + ), + ('content', models.TextField(verbose_name='content')), + ], + options={ + 'verbose_name': 'Comment', + 'verbose_name_plural': 'Comments', + }, + ), + ] diff --git a/backend/apps/comment/migrations/0002_initial.py b/backend/apps/comment/migrations/0002_initial.py new file mode 100644 index 0000000..69f05c2 --- /dev/null +++ b/backend/apps/comment/migrations/0002_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 5.1.4 on 2024-12-13 12:02 + +import django.contrib.postgres.indexes +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('comment', '0001_initial'), + ('user', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='downvotes', + field=models.ManyToManyField( + blank=True, + related_name='downvotes', + to='user.profile', + verbose_name='downvotes', + ), + ), + migrations.AddField( + model_name='comment', + name='quibbler', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='user.profile', + verbose_name='quibbler', + ), + ), + migrations.AddField( + model_name='comment', + name='upvotes', + field=models.ManyToManyField( + blank=True, + related_name='upvotes', + to='user.profile', + verbose_name='upvotes', + ), + ), + migrations.AddIndex( + model_name='comment', + index=django.contrib.postgres.indexes.GistIndex( + fields=['path'], name='comment_com_path_d1388c_gist' + ), + ), + ] diff --git a/backend/apps/comment/migrations/__init__.py b/backend/apps/comment/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/comment/models.py b/backend/apps/comment/models.py new file mode 100644 index 0000000..46cba3d --- /dev/null +++ b/backend/apps/comment/models.py @@ -0,0 +1,34 @@ +from django.contrib.postgres import indexes as idx +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_ltree.models import TreeModel + +from apps.user.models import Profile +from common.mixins.model_mixins import CreatedAtMixin + +# Create your models here. + + +class Comment(CreatedAtMixin, TreeModel): + quibbler = models.ForeignKey( + Profile, on_delete=models.CASCADE, verbose_name=_('quibbler') + ) + content = models.TextField(_('content')) + upvotes = models.ManyToManyField( + Profile, related_name='upvotes', blank=True, verbose_name=_('upvotes') + ) + downvotes = models.ManyToManyField( + Profile, related_name='downvotes', blank=True, verbose_name=_('downvotes') + ) + + @property + def children_count(self): + return self.children().count() + + def __str__(self) -> str: + return f"Comment by {self.quibbler.username}" + + class Meta: # pyright: ignore + indexes = [idx.GistIndex(fields=['path'])] + verbose_name = 'Comment' + verbose_name_plural = 'Comments' diff --git a/backend/apps/quiblet/api/v1/serializers.py b/backend/apps/quiblet/api/v1/serializers.py index 6011cdb..0edfd11 100644 --- a/backend/apps/quiblet/api/v1/serializers.py +++ b/backend/apps/quiblet/api/v1/serializers.py @@ -18,7 +18,23 @@ def validate_name(self, name): return name +class QuibletSlimSerializer(ModelSerializer): + class Meta: + model = Quiblet + fields = ('name', 'avatar') + + class QuibSerializer(ModelSerializer): + quiblet = QuibletSerializer(read_only=True) + class Meta: model = Quib fields = '__all__' + + +class QuibSlimSerializer(ModelSerializer): + quiblet = QuibletSlimSerializer(read_only=True) + + class Meta: + model = Quib + exclude = ('quibber',) diff --git a/backend/apps/quiblet/api/v1/viewsets.py b/backend/apps/quiblet/api/v1/viewsets.py index 002e4eb..d928070 100644 --- a/backend/apps/quiblet/api/v1/viewsets.py +++ b/backend/apps/quiblet/api/v1/viewsets.py @@ -6,7 +6,7 @@ from common.patches.request import PatchedHttpRequest from ...models import Quib, Quiblet -from .serializers import QuibletSerializer, QuibSerializer +from .serializers import QuibletSerializer, QuibSerializer, QuibSlimSerializer @extend_schema(tags=['quibs & quiblets']) @@ -28,4 +28,8 @@ def perform_create(self, serializer): @extend_schema(tags=['quibs & quiblets']) class QuibViewSet(ModelViewSet): queryset = Quib.objects.all() - serializer_class = QuibSerializer + + def get_serializer_class(self): # pyright: ignore + if self.action == 'list': + return QuibSlimSerializer + return QuibSerializer diff --git a/backend/apps/quiblet/migrations/0001_initial.py b/backend/apps/quiblet/migrations/0001_initial.py index 18bae33..a930159 100644 --- a/backend/apps/quiblet/migrations/0001_initial.py +++ b/backend/apps/quiblet/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-11 05:30 +# Generated by Django 5.1.4 on 2024-12-13 12:02 import dynamic_filenames import shortuuid.django_fields @@ -9,45 +9,11 @@ class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [ + ('comment', '0001_initial'), + ] operations = [ - migrations.CreateModel( - name='Quib', - fields=[ - ( - 'created_at', - models.DateTimeField(auto_now_add=True, verbose_name='create at'), - ), - ('is_public', models.BooleanField(default=True, verbose_name='is public')), - ( - 'id', - shortuuid.django_fields.ShortUUIDField( - alphabet='abcdefghijklmnopqrstuvwxyz0123456789', - editable=False, - length=7, - max_length=7, - prefix='', - primary_key=True, - serialize=False, - verbose_name='id', - ), - ), - ('title', models.CharField(max_length=255, verbose_name='title')), - ( - 'slug', - models.SlugField( - blank=True, editable=False, max_length=25, verbose_name='slug' - ), - ), - ('content', models.TextField(verbose_name='content')), - ], - options={ - 'verbose_name': 'Quib', - 'verbose_name_plural': 'Quibs', - 'ordering': ['-created_at'], - }, - ), migrations.CreateModel( name='Quiblet', fields=[ @@ -96,4 +62,60 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), + migrations.CreateModel( + name='Quib', + fields=[ + ( + 'created_at', + models.DateTimeField(auto_now_add=True, verbose_name='create at'), + ), + ('is_public', models.BooleanField(default=True, verbose_name='is public')), + ( + 'id', + shortuuid.django_fields.ShortUUIDField( + alphabet='abcdefghijklmnopqrstuvwxyz0123456789', + editable=False, + length=7, + max_length=7, + prefix='', + primary_key=True, + serialize=False, + verbose_name='id', + ), + ), + ('title', models.CharField(max_length=255, verbose_name='title')), + ( + 'slug', + models.SlugField( + blank=True, editable=False, max_length=25, verbose_name='slug' + ), + ), + ('content', models.TextField(verbose_name='content')), + ( + 'cover', + models.ImageField( + blank=True, + null=True, + upload_to=dynamic_filenames.FilePattern( + filename_pattern='cover/{uuid:s}{ext}' + ), + verbose_name='cover', + ), + ), + ( + 'comments', + models.ManyToManyField( + blank=True, + related_name='comments', + to='comment.comment', + verbose_name='comments', + ), + ), + ], + options={ + 'verbose_name': 'Quib', + 'verbose_name_plural': 'Quibs', + 'ordering': ['-created_at'], + }, + ), ] diff --git a/backend/apps/quiblet/migrations/0002_initial.py b/backend/apps/quiblet/migrations/0002_initial.py index 71e6e0b..c26cceb 100644 --- a/backend/apps/quiblet/migrations/0002_initial.py +++ b/backend/apps/quiblet/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-11 05:30 +# Generated by Django 5.1.4 on 2024-12-13 12:02 import django.db.models.deletion import django.db.models.functions.text @@ -17,22 +17,12 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='quib', - name='dislikes', + name='downvotes', field=models.ManyToManyField( blank=True, - related_name='disliked_quibs', + related_name='downvoted_quibs', to='user.profile', - verbose_name='dislikes', - ), - ), - migrations.AddField( - model_name='quib', - name='likes', - field=models.ManyToManyField( - blank=True, - related_name='liked_quibs', - to='user.profile', - verbose_name='likes', + verbose_name='downvotes', ), ), migrations.AddField( @@ -45,6 +35,16 @@ class Migration(migrations.Migration): verbose_name='quibber', ), ), + migrations.AddField( + model_name='quib', + name='upvotes', + field=models.ManyToManyField( + blank=True, + related_name='upvoted_quibs', + to='user.profile', + verbose_name='upvotes', + ), + ), migrations.AddField( model_name='quiblet', name='members', diff --git a/backend/apps/quiblet/models.py b/backend/apps/quiblet/models.py index 0ed343a..f2bf678 100644 --- a/backend/apps/quiblet/models.py +++ b/backend/apps/quiblet/models.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from dynamic_filenames import FilePattern +from apps.comment.models import Comment from apps.user.models import Profile from common.mixins.model_mixins import ( AvatarMixin, @@ -13,13 +14,17 @@ ShortUUIDMixin, ) +cover_file_pattern = FilePattern(filename_pattern="cover/{uuid:s}{ext}") + +# Create your models here. + class Quiblet(AvatarMixin, CreatedAtMixin, IsPublicMixin): name = models.CharField(_('name'), unique=True, max_length=25) description = models.TextField(_('description')) cover = models.ImageField( _('cover'), - upload_to=FilePattern(filename_pattern="cover/{uuid:s}{ext}"), + upload_to=cover_file_pattern, blank=True, null=True, ) @@ -58,11 +63,20 @@ class Quib(CreatedAtMixin, IsPublicMixin, ShortUUIDMixin): title = models.CharField(_('title'), max_length=255) slug = models.SlugField(_('slug'), editable=False, max_length=25, blank=True) content = models.TextField(_('content')) - likes = models.ManyToManyField( - Profile, related_name='liked_quibs', blank=True, verbose_name=_('likes') + cover = models.ImageField( + _('cover'), + upload_to=cover_file_pattern, + blank=True, + null=True, + ) + upvotes = models.ManyToManyField( + Profile, related_name='upvoted_quibs', blank=True, verbose_name=_('upvotes') + ) + downvotes = models.ManyToManyField( + Profile, related_name='downvoted_quibs', blank=True, verbose_name=_('downvotes') ) - dislikes = models.ManyToManyField( - Profile, related_name='disliked_quibs', blank=True, verbose_name=_('dislikes') + comments = models.ManyToManyField( + Comment, related_name='comments', blank=True, verbose_name=_('comments') ) def save(self, *args, **kwargs): diff --git a/backend/apps/user/migrations/0001_initial.py b/backend/apps/user/migrations/0001_initial.py index 6627029..deb2aef 100644 --- a/backend/apps/user/migrations/0001_initial.py +++ b/backend/apps/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-11 05:30 +# Generated by Django 5.1.4 on 2024-12-13 12:02 import functools @@ -8,6 +8,7 @@ from django.db import migrations, models import apps.user.managers +import apps.user.validators import common.mixins.model_mixins @@ -165,7 +166,16 @@ class Migration(migrations.Migration): ), ( 'username', - models.CharField(max_length=25, unique=True, verbose_name='username'), + models.CharField( + error_messages={ + 'unique': 'A profile with that username already exists.' + }, + help_text='Required. 25 characters or fewer. Letters, digits and ./_ only.', + max_length=25, + unique=True, + validators=[apps.user.validators.UsernameValidator()], + verbose_name='username', + ), ), ( 'first_name', diff --git a/backend/apps/user/migrations/0002_alter_profile_username.py b/backend/apps/user/migrations/0002_alter_profile_username.py deleted file mode 100644 index 2e56921..0000000 --- a/backend/apps/user/migrations/0002_alter_profile_username.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-11 15:44 - -from django.db import migrations, models - -import apps.user.validators - - -class Migration(migrations.Migration): - - dependencies = [ - ('user', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='profile', - name='username', - field=models.CharField( - max_length=25, - unique=True, - validators=[apps.user.validators.UsernameValidator()], - verbose_name='username', - ), - ), - ] diff --git a/backend/apps/user/migrations/0003_alter_profile_username.py b/backend/apps/user/migrations/0003_alter_profile_username.py deleted file mode 100644 index a0e1336..0000000 --- a/backend/apps/user/migrations/0003_alter_profile_username.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-11 15:47 - -from django.db import migrations, models - -import apps.user.validators - - -class Migration(migrations.Migration): - - dependencies = [ - ('user', '0002_alter_profile_username'), - ] - - operations = [ - migrations.AlterField( - model_name='profile', - name='username', - field=models.CharField( - error_messages={'unique': 'A profile with that username already exists.'}, - help_text='Required. 25 characters or fewer. Letters, digits and ./_ only.', - max_length=25, - unique=True, - validators=[apps.user.validators.UsernameValidator()], - verbose_name='username', - ), - ), - ] diff --git a/backend/config/settings.py b/backend/config/settings.py index 64fd9d7..053a536 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -53,11 +53,14 @@ 'drf_standardized_errors', # file middleware 'django_cleanup', + # postgres ltree + 'django_ltree', ] SELF_APPS = [ 'apps.user', 'apps.quiblet', + 'apps.comment', ] INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + SELF_APPS diff --git a/backend/config/urls.py b/backend/config/urls.py index 7f48df5..682679e 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -25,6 +25,8 @@ path('u/', include('apps.user.api.v1.urls')), # quiblet and quib endpoints path('q/', include('apps.quiblet.api.v1.urls')), + # comments + path('comments/', include('apps.comment.api.v1.urls')), ] ), ), diff --git a/backend/poetry.lock b/backend/poetry.lock index ae429d7..7e8f5ff 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "asgiref" @@ -283,6 +283,21 @@ files = [ [package.dependencies] Django = ">=4.2" +[[package]] +name = "django-ltree-2" +version = "0.1.10" +description = "Continual of django-ltree" +optional = false +python-versions = ">=3.9" +files = [ + {file = "django_ltree_2-0.1.10-py3-none-any.whl", hash = "sha256:1b3b51c5b97beda5254cdf0dcef83a5c4258c791e3b2b6c331d1e52a8b1dd6a2"}, + {file = "django_ltree_2-0.1.10.tar.gz", hash = "sha256:1540aced6564429865b6330dfe7bbacbb3cedfdff3823b546859265117c9f048"}, +] + +[package.dependencies] +django = ">=3.2" +psycopg = ">=3" + [[package]] name = "django-rest-knox" version = "5.0.2" @@ -701,6 +716,29 @@ pyyaml = ">=6.0.2,<7.0.0" [package.extras] poetry-plugin = ["poetry (>=1.0,<2.0)"] +[[package]] +name = "psycopg" +version = "3.2.3" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, + {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.3)"] +c = ["psycopg-c (==3.2.3)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -1166,4 +1204,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "4c73b102a188bd740568beb8009f232e6bd7dc9dfc700de4649ba2cc0fc04a92" +content-hash = "54ccfc824f80b2d5789b1639f0de254d0a1d0c49737d837e220755155d8b6671" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c4917e0..42f582e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -33,6 +33,7 @@ drf-spectacular = {extras = ["sidecar"], version = "^0.28.0"} shortuuid = "^1.0.13" # 3rd party exception handler drf-standardized-errors = {extras = ["openapi"], version = "^0.14.1"} +django-ltree-2 = "^0.1.10" [tool.poetry.group.dev.dependencies] # task runner diff --git a/frontend/src/lib/clients/v1.ts b/frontend/src/lib/clients/v1.ts index 2870757..494686c 100644 --- a/frontend/src/lib/clients/v1.ts +++ b/frontend/src/lib/clients/v1.ts @@ -320,6 +320,7 @@ export interface components { color?: components['schemas']['ColorEnum']; /** Format: uri */ avatar?: string | null; + /** @description Required. 25 characters or fewer. Letters, digits and ./_ only. */ username?: string; first_name?: string | null; last_name?: string | null; @@ -327,13 +328,15 @@ export interface components { }; PatchedQuib: { readonly id?: string; + readonly quiblet?: components['schemas']['Quiblet']; /** Format: date-time */ readonly created_at?: string; is_public?: boolean; title?: string; readonly slug?: string; content?: string; - quiblet?: number; + /** Format: uri */ + cover?: string | null; /** Quibbler */ quibber?: number; likes?: number[]; @@ -361,6 +364,7 @@ export interface components { color?: components['schemas']['ColorEnum']; /** Format: uri */ avatar?: string | null; + /** @description Required. 25 characters or fewer. Letters, digits and ./_ only. */ username: string; first_name?: string | null; last_name?: string | null; @@ -867,6 +871,23 @@ export interface components { | 'surrogate_characters_not_allowed'; detail: string; }; + QQuibsCreateCoverErrorComponent: { + /** + * @description * `cover` - cover (enum property replaced by openapi-typescript) + * @enum {string} + */ + attr: 'cover'; + /** + * @description * `empty` - empty + * * `invalid` - invalid + * * `invalid_image` - invalid_image + * * `max_length` - max_length + * * `no_name` - no_name + * @enum {string} + */ + code: 'empty' | 'invalid' | 'invalid_image' | 'max_length' | 'no_name'; + detail: string; + }; QQuibsCreateDislikesErrorComponent: { /** * @description * `dislikes` - dislikes (enum property replaced by openapi-typescript) @@ -888,7 +909,7 @@ export interface components { | components['schemas']['QQuibsCreateIsPublicErrorComponent'] | components['schemas']['QQuibsCreateTitleErrorComponent'] | components['schemas']['QQuibsCreateContentErrorComponent'] - | components['schemas']['QQuibsCreateQuibletErrorComponent'] + | components['schemas']['QQuibsCreateCoverErrorComponent'] | components['schemas']['QQuibsCreateQuibberErrorComponent'] | components['schemas']['QQuibsCreateLikesErrorComponent'] | components['schemas']['QQuibsCreateDislikesErrorComponent']; @@ -952,22 +973,6 @@ export interface components { code: 'does_not_exist' | 'incorrect_type' | 'null' | 'required'; detail: string; }; - QQuibsCreateQuibletErrorComponent: { - /** - * @description * `quiblet` - quiblet (enum property replaced by openapi-typescript) - * @enum {string} - */ - attr: 'quiblet'; - /** - * @description * `does_not_exist` - does_not_exist - * * `incorrect_type` - incorrect_type - * * `null` - null - * * `required` - required - * @enum {string} - */ - code: 'does_not_exist' | 'incorrect_type' | 'null' | 'required'; - detail: string; - }; QQuibsCreateTitleErrorComponent: { /** * @description * `title` - title (enum property replaced by openapi-typescript) @@ -1022,6 +1027,23 @@ export interface components { | 'surrogate_characters_not_allowed'; detail: string; }; + QQuibsPartialUpdateCoverErrorComponent: { + /** + * @description * `cover` - cover (enum property replaced by openapi-typescript) + * @enum {string} + */ + attr: 'cover'; + /** + * @description * `empty` - empty + * * `invalid` - invalid + * * `invalid_image` - invalid_image + * * `max_length` - max_length + * * `no_name` - no_name + * @enum {string} + */ + code: 'empty' | 'invalid' | 'invalid_image' | 'max_length' | 'no_name'; + detail: string; + }; QQuibsPartialUpdateDislikesErrorComponent: { /** * @description * `dislikes` - dislikes (enum property replaced by openapi-typescript) @@ -1043,7 +1065,7 @@ export interface components { | components['schemas']['QQuibsPartialUpdateIsPublicErrorComponent'] | components['schemas']['QQuibsPartialUpdateTitleErrorComponent'] | components['schemas']['QQuibsPartialUpdateContentErrorComponent'] - | components['schemas']['QQuibsPartialUpdateQuibletErrorComponent'] + | components['schemas']['QQuibsPartialUpdateCoverErrorComponent'] | components['schemas']['QQuibsPartialUpdateQuibberErrorComponent'] | components['schemas']['QQuibsPartialUpdateLikesErrorComponent'] | components['schemas']['QQuibsPartialUpdateDislikesErrorComponent']; @@ -1107,22 +1129,6 @@ export interface components { code: 'does_not_exist' | 'incorrect_type' | 'null' | 'required'; detail: string; }; - QQuibsPartialUpdateQuibletErrorComponent: { - /** - * @description * `quiblet` - quiblet (enum property replaced by openapi-typescript) - * @enum {string} - */ - attr: 'quiblet'; - /** - * @description * `does_not_exist` - does_not_exist - * * `incorrect_type` - incorrect_type - * * `null` - null - * * `required` - required - * @enum {string} - */ - code: 'does_not_exist' | 'incorrect_type' | 'null' | 'required'; - detail: string; - }; QQuibsPartialUpdateTitleErrorComponent: { /** * @description * `title` - title (enum property replaced by openapi-typescript) @@ -1177,6 +1183,23 @@ export interface components { | 'surrogate_characters_not_allowed'; detail: string; }; + QQuibsUpdateCoverErrorComponent: { + /** + * @description * `cover` - cover (enum property replaced by openapi-typescript) + * @enum {string} + */ + attr: 'cover'; + /** + * @description * `empty` - empty + * * `invalid` - invalid + * * `invalid_image` - invalid_image + * * `max_length` - max_length + * * `no_name` - no_name + * @enum {string} + */ + code: 'empty' | 'invalid' | 'invalid_image' | 'max_length' | 'no_name'; + detail: string; + }; QQuibsUpdateDislikesErrorComponent: { /** * @description * `dislikes` - dislikes (enum property replaced by openapi-typescript) @@ -1198,7 +1221,7 @@ export interface components { | components['schemas']['QQuibsUpdateIsPublicErrorComponent'] | components['schemas']['QQuibsUpdateTitleErrorComponent'] | components['schemas']['QQuibsUpdateContentErrorComponent'] - | components['schemas']['QQuibsUpdateQuibletErrorComponent'] + | components['schemas']['QQuibsUpdateCoverErrorComponent'] | components['schemas']['QQuibsUpdateQuibberErrorComponent'] | components['schemas']['QQuibsUpdateLikesErrorComponent'] | components['schemas']['QQuibsUpdateDislikesErrorComponent']; @@ -1262,22 +1285,6 @@ export interface components { code: 'does_not_exist' | 'incorrect_type' | 'null' | 'required'; detail: string; }; - QQuibsUpdateQuibletErrorComponent: { - /** - * @description * `quiblet` - quiblet (enum property replaced by openapi-typescript) - * @enum {string} - */ - attr: 'quiblet'; - /** - * @description * `does_not_exist` - does_not_exist - * * `incorrect_type` - incorrect_type - * * `null` - null - * * `required` - required - * @enum {string} - */ - code: 'does_not_exist' | 'incorrect_type' | 'null' | 'required'; - detail: string; - }; QQuibsUpdateTitleErrorComponent: { /** * @description * `title` - title (enum property replaced by openapi-typescript) @@ -1310,18 +1317,34 @@ export interface components { }; Quib: { readonly id: string; + readonly quiblet: components['schemas']['Quiblet']; /** Format: date-time */ readonly created_at: string; is_public?: boolean; title: string; readonly slug: string; content: string; - quiblet: number; + /** Format: uri */ + cover?: string | null; /** Quibbler */ quibber: number; likes?: number[]; dislikes?: number[]; }; + QuibSlim: { + readonly id: string; + readonly quiblet: components['schemas']['QuibletSlim']; + /** Format: date-time */ + readonly created_at: string; + is_public?: boolean; + title: string; + readonly slug: string; + content: string; + /** Format: uri */ + cover?: string | null; + likes?: number[]; + dislikes?: number[]; + }; Quiblet: { readonly id: number; /** Format: date-time */ @@ -1336,6 +1359,11 @@ export interface components { members?: number[]; rangers?: number[]; }; + QuibletSlim: { + name: string; + /** Format: uri */ + avatar?: string | null; + }; /** * @description * `server_error` - Server Error * @enum {string} @@ -2199,7 +2227,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['Quib'][]; + 'application/json': components['schemas']['QuibSlim'][]; }; }; 500: { diff --git a/frontend/src/lib/components/pages/home/post.svelte b/frontend/src/lib/components/pages/home/quib.svelte similarity index 70% rename from frontend/src/lib/components/pages/home/post.svelte rename to frontend/src/lib/components/pages/home/quib.svelte index 4ade6bb..a4cf559 100644 --- a/frontend/src/lib/components/pages/home/post.svelte +++ b/frontend/src/lib/components/pages/home/quib.svelte @@ -1,22 +1,9 @@ @@ -25,14 +12,14 @@ class="relative flex flex-col gap-2 rounded-2xl border border-neutral bg-base-300 p-4 transition-colors hover:bg-base-200" >
- - -

q/{props.community.name}

+
+ +

q/{props.quiblet.name}

@@ -50,15 +37,15 @@