From c7b68db7161673632dbc11244c031aba44a663ce Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Wed, 8 May 2024 14:17:05 -0400 Subject: [PATCH 01/16] Added django model --- backend/coreapp/models/comment.py | 26 +++++++++++ backend/coreapp/tests/test_comment.py | 4 ++ backend/coreapp/views/comment.py | 43 +++++++++++++++++++ .../Scratch/CommentsPanel.module.scss | 0 .../src/components/Scratch/CommentsPanel.tsx | 14 ++++++ frontend/src/components/Scratch/Scratch.tsx | 7 +++ 6 files changed, 94 insertions(+) create mode 100644 backend/coreapp/models/comment.py create mode 100644 backend/coreapp/tests/test_comment.py create mode 100644 backend/coreapp/views/comment.py create mode 100644 frontend/src/components/Scratch/CommentsPanel.module.scss create mode 100644 frontend/src/components/Scratch/CommentsPanel.tsx diff --git a/backend/coreapp/models/comment.py b/backend/coreapp/models/comment.py new file mode 100644 index 000000000..6cd925f8a --- /dev/null +++ b/backend/coreapp/models/comment.py @@ -0,0 +1,26 @@ +from django.db import models +from django.utils.crypto import get_random_string +from .profile import Profile +from .scratch import Scratch + + +def gen_comment_id() -> str: + return get_random_string(length=32) + + +class Comment(models.Model): + slug = models.SlugField(primary_key=True, default=gen_comment_id) + scratch = models.ForeignKey( + Scratch, null=False, blank=False, on_delete=models.SET_NULL) + owner = models.ForeignKey( + Profile, null=False, blank=False, on_delete=models.SET_NULL) + text = models.TextField(max_length=5000) + creation_time = models.DateTimeField(auto_now_add=True) + # TODO: Add replies + # TODO: Add last_updated for editing + + class Meta: + ordering = ["-creation_time"] + + def __str__(self) -> str: + return self.slug diff --git a/backend/coreapp/tests/test_comment.py b/backend/coreapp/tests/test_comment.py new file mode 100644 index 000000000..560d6c8fd --- /dev/null +++ b/backend/coreapp/tests/test_comment.py @@ -0,0 +1,4 @@ +from coreapp.tests.common import BaseTestCase + +class CommentTest(BaseTestCase): + pass diff --git a/backend/coreapp/views/comment.py b/backend/coreapp/views/comment.py new file mode 100644 index 000000000..3b2337299 --- /dev/null +++ b/backend/coreapp/views/comment.py @@ -0,0 +1,43 @@ +import django_filters +from rest_framework.pagination import CursorPagination +from rest_framework import mixins, filters +from rest_framework.viewsets import GenericViewSet +from django.http import HttpResponseForbidden +from ..models.scratch import Scratch +from ..models.comment import Comment + + +class CommentPagination(CursorPagination): + ordering = "-creation_time" + page_size = 50 + page_size_query_param = "page_size" + max_page_size = 100 + + +class CommentViewSet( + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + GenericViewSet, # type: ignore +): + queryset = Comment.objects.all() + pagination_class = CommentPagination + filterset_fields = ["scratch"] + filter_backends = [ + django_filters.rest_framework.DjangoFilterBackend, + filters.SearchFilter, + ] + + def post(self, request, *args, **kwargs): + if not request.user.is_authenticated: + return HttpResponseForbidden() + + # Look up the author we're interested in. + self.object = self.get_object() + # Actually record interest somehow here! + + return HttpResponseRedirect( + reverse("author-detail", kwargs={"pk": self.object.pk}) + ) diff --git a/frontend/src/components/Scratch/CommentsPanel.module.scss b/frontend/src/components/Scratch/CommentsPanel.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/Scratch/CommentsPanel.tsx b/frontend/src/components/Scratch/CommentsPanel.tsx new file mode 100644 index 000000000..eab92ccb9 --- /dev/null +++ b/frontend/src/components/Scratch/CommentsPanel.tsx @@ -0,0 +1,14 @@ +import { TerseScratch } from "@/lib/api" + +import { styles } from "./CommentsPanel.module.scss" + +type Props = { + scratch: TerseScratch +} + +export default function CommentsPanel({ scratch }): Props { + return ( +
+
+ ) +} diff --git a/frontend/src/components/Scratch/Scratch.tsx b/frontend/src/components/Scratch/Scratch.tsx index 2d09c9de5..71fa643ba 100644 --- a/frontend/src/components/Scratch/Scratch.tsx +++ b/frontend/src/components/Scratch/Scratch.tsx @@ -26,6 +26,7 @@ import styles from "./Scratch.module.scss" import ScratchMatchBanner from "./ScratchMatchBanner" import ScratchProgressBar from "./ScratchProgressBar" import ScratchToolbar from "./ScratchToolbar" +import CommentsPanel from "./CommentsPanel" enum TabId { ABOUT = "scratch_about", @@ -35,6 +36,7 @@ enum TabId { DIFF = "scratch_diff", DECOMPILATION = "scratch_decompilation", FAMILY = "scratch_family", + COMMENTS = "scratch_comments", } const DEFAULT_LAYOUTS: Record<"desktop_2col" | "mobile_2row", Layout> = { @@ -54,6 +56,7 @@ const DEFAULT_LAYOUTS: Record<"desktop_2col" | "mobile_2row", Layout> = { TabId.SOURCE_CODE, TabId.CONTEXT, TabId.OPTIONS, + TabId.COMMENTS, ], }, { @@ -296,6 +299,10 @@ export default function Scratch({ return {() => } + case TabId.COMMENTS: + return + {() => } + default: return } From 6a98940a1c25da101a7d386e4e4227c905501b1d Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Wed, 8 May 2024 16:43:47 -0400 Subject: [PATCH 02/16] Added more stuff --- backend/coreapp/admin.py | 2 + backend/coreapp/migrations/0054_comment.py | 48 ++++++++++++++++++++++ backend/coreapp/models/comment.py | 4 +- backend/coreapp/serializers.py | 3 ++ backend/coreapp/urls.py | 2 + backend/coreapp/views/comment.py | 47 ++++++++++++++++----- 6 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 backend/coreapp/migrations/0054_comment.py diff --git a/backend/coreapp/admin.py b/backend/coreapp/admin.py index b076f08fe..41b24b46e 100644 --- a/backend/coreapp/admin.py +++ b/backend/coreapp/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin +from .models.comment import Comment from .models.course import Course, CourseChapter, CourseScenario from .models.github import GitHubUser from .models.preset import Preset @@ -26,3 +27,4 @@ admin.site.register(Course) admin.site.register(CourseChapter) admin.site.register(CourseScenario) +admin.site.register(Comment) diff --git a/backend/coreapp/migrations/0054_comment.py b/backend/coreapp/migrations/0054_comment.py new file mode 100644 index 000000000..e7fe1b16f --- /dev/null +++ b/backend/coreapp/migrations/0054_comment.py @@ -0,0 +1,48 @@ +# Generated by Django 5.0.3 on 2024-05-08 19:59 + +import coreapp.models.comment +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("coreapp", "0053_rename_mwcps2_compilers"), + ] + + operations = [ + migrations.CreateModel( + name="Comment", + fields=[ + ( + "slug", + models.SlugField( + default=coreapp.models.comment.gen_comment_id, + primary_key=True, + serialize=False, + ), + ), + ("text", models.TextField(max_length=5000)), + ("creation_time", models.DateTimeField(auto_now_add=True)), + ( + "owner", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="coreapp.profile", + ), + ), + ( + "scratch", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="coreapp.scratch", + ), + ), + ], + options={ + "ordering": ["-creation_time"], + }, + ), + ] diff --git a/backend/coreapp/models/comment.py b/backend/coreapp/models/comment.py index 6cd925f8a..9c55e6e99 100644 --- a/backend/coreapp/models/comment.py +++ b/backend/coreapp/models/comment.py @@ -11,9 +11,9 @@ def gen_comment_id() -> str: class Comment(models.Model): slug = models.SlugField(primary_key=True, default=gen_comment_id) scratch = models.ForeignKey( - Scratch, null=False, blank=False, on_delete=models.SET_NULL) + Scratch, null=True, blank=False, on_delete=models.SET_NULL) owner = models.ForeignKey( - Profile, null=False, blank=False, on_delete=models.SET_NULL) + Profile, null=True, blank=False, on_delete=models.SET_NULL) text = models.TextField(max_length=5000) creation_time = models.DateTimeField(auto_now_add=True) # TODO: Add replies diff --git a/backend/coreapp/serializers.py b/backend/coreapp/serializers.py index 880588bf8..ea675fa38 100644 --- a/backend/coreapp/serializers.py +++ b/backend/coreapp/serializers.py @@ -316,3 +316,6 @@ class ProjectMemberSerializer(serializers.ModelSerializer[ProjectMember]): class Meta: model = ProjectMember fields = ["username"] + +class CommentSerializer(serializers.ModelSerializer[Comment]): + owner = diff --git a/backend/coreapp/urls.py b/backend/coreapp/urls.py index 6e270e9f5..5c2bb5238 100644 --- a/backend/coreapp/urls.py +++ b/backend/coreapp/urls.py @@ -9,6 +9,7 @@ project, scratch, user, + comment, ) urlpatterns = [ @@ -24,6 +25,7 @@ *scratch.router.urls, *preset.router.urls, *project.router.urls, + *comment.router.urls, path("user", user.CurrentUser.as_view(), name="current-user"), path( "user/scratches", diff --git a/backend/coreapp/views/comment.py b/backend/coreapp/views/comment.py index 3b2337299..5051891f1 100644 --- a/backend/coreapp/views/comment.py +++ b/backend/coreapp/views/comment.py @@ -1,10 +1,23 @@ +from typing import Any, Optional + import django_filters from rest_framework.pagination import CursorPagination -from rest_framework import mixins, filters +from rest_framework import mixins, filters, status from rest_framework.viewsets import GenericViewSet -from django.http import HttpResponseForbidden -from ..models.scratch import Scratch +from rest_framework.response import Response +from rest_framework.request import Request +from rest_framework.exceptions import APIException +# from django.http import HttpResponseForbidden +# from ..models.scratch import Scratch from ..models.comment import Comment +from django.contrib.auth.models import User + +from ..models.github import GitHubUser + + +class GithubLoginException(APIException): + status_code = status.HTTP_403_FORBIDDEN + default_detail = "You must be logged in to Github to perform this action." class CommentPagination(CursorPagination): @@ -30,14 +43,26 @@ class CommentViewSet( filters.SearchFilter, ] - def post(self, request, *args, **kwargs): - if not request.user.is_authenticated: - return HttpResponseForbidden() + def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: + user: Optional[User] = request.profile.user + if not user: + raise GithubLoginException() + gh_user: Optional[GitHubUser] = user.github + if not gh_user: + raise GithubLoginException() + + serializer = ProjectSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + slug = serializer.validated_data["slug"] + if slug == "new" or Project.objects.filter(slug=slug).exists(): + raise ProjectExistsException() + + project = serializer.save() - # Look up the author we're interested in. - self.object = self.get_object() - # Actually record interest somehow here! + ProjectMember(project=project, user=request.profile.user).save() - return HttpResponseRedirect( - reverse("author-detail", kwargs={"pk": self.object.pk}) + return Response( + ProjectSerializer(project, context={"request": request}).data, + status=status.HTTP_201_CREATED, ) From 88585253186a24d8f04d05f92fc222f8407304e2 Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Thu, 9 May 2024 15:41:20 -0400 Subject: [PATCH 03/16] Added Comment Panel and frontend API's --- backend/coreapp/serializers.py | 26 +++++++- backend/coreapp/urls.py | 10 ++++ backend/coreapp/views/comment.py | 23 ++++---- backend/coreapp/views/user.py | 28 ++++++++- ...Panel.module.scss => Comments.module.scss} | 0 frontend/src/components/Scratch/Comments.tsx | 59 +++++++++++++++++++ .../src/components/Scratch/CommentsPanel.tsx | 13 +++- frontend/src/lib/api.ts | 13 ++++ frontend/src/lib/api/types.ts | 7 +++ 9 files changed, 165 insertions(+), 14 deletions(-) rename frontend/src/components/Scratch/{CommentsPanel.module.scss => Comments.module.scss} (100%) create mode 100644 frontend/src/components/Scratch/Comments.tsx diff --git a/backend/coreapp/serializers.py b/backend/coreapp/serializers.py index ea675fa38..17064108d 100644 --- a/backend/coreapp/serializers.py +++ b/backend/coreapp/serializers.py @@ -20,6 +20,7 @@ from .models.profile import Profile from .models.project import Project, ProjectMember from .models.scratch import Scratch +from .models.comment import Comment def serialize_profile(request: Request, profile: Profile) -> Dict[str, Any]: @@ -317,5 +318,28 @@ class Meta: model = ProjectMember fields = ["username"] + class CommentSerializer(serializers.ModelSerializer[Comment]): - owner = + owner = ProfileField(read_only=True) + text = serializers.CharField(allow_blank=True, trim_whitespace=False) + + class Meta: + model = Comment + fields = [ + "slug", + "text", + "owner", + "scratch", + "creation_time", + ] + + def create(self, validated_data: Any) -> Comment: + comment = Project.objects.create(**validated_data) + return comment + + def validate(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + TODO: Validate that the scratch and the user are both valid to allow comment + creation + """ + return True diff --git a/backend/coreapp/urls.py b/backend/coreapp/urls.py index 5c2bb5238..00a00cc31 100644 --- a/backend/coreapp/urls.py +++ b/backend/coreapp/urls.py @@ -32,12 +32,22 @@ user.CurrentUserScratchList.as_view(), name="current-user-scratches", ), + path( + "user/comments", + user.CurrentUserCommentList.as_view(), + name="current-user-comments", + ), path("users/", user.user, name="user-detail"), path( "users//scratches", user.UserScratchList.as_view(), name="user-scratches", ), + path( + "users//comments", + user.UserCommentList.as_view(), + name="user-comments", + ), # TODO: remove path("compilers", compiler.CompilerDetail.as_view(), name="compilers"), path("libraries", library.LibraryDetail.as_view(), name="libraries"), diff --git a/backend/coreapp/views/comment.py b/backend/coreapp/views/comment.py index 5051891f1..78d29c0e9 100644 --- a/backend/coreapp/views/comment.py +++ b/backend/coreapp/views/comment.py @@ -1,18 +1,20 @@ from typing import Any, Optional import django_filters + from rest_framework.pagination import CursorPagination from rest_framework import mixins, filters, status from rest_framework.viewsets import GenericViewSet from rest_framework.response import Response from rest_framework.request import Request from rest_framework.exceptions import APIException +from rest_framework.routers import DefaultRouter # from django.http import HttpResponseForbidden # from ..models.scratch import Scratch from ..models.comment import Comment -from django.contrib.auth.models import User - from ..models.github import GitHubUser +from ..serializers import CommentSerializer +from django.contrib.auth.models import User class GithubLoginException(APIException): @@ -42,6 +44,7 @@ class CommentViewSet( django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter, ] + serializer_class = CommentSerializer def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: user: Optional[User] = request.profile.user @@ -51,18 +54,16 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: if not gh_user: raise GithubLoginException() - serializer = ProjectSerializer(data=request.data) + serializer = CommentSerializer(data=request.data) serializer.is_valid(raise_exception=True) - slug = serializer.validated_data["slug"] - if slug == "new" or Project.objects.filter(slug=slug).exists(): - raise ProjectExistsException() - - project = serializer.save() - - ProjectMember(project=project, user=request.profile.user).save() + comment = serializer.save() return Response( - ProjectSerializer(project, context={"request": request}).data, + CommentSerializer(comment, context={"request": request}).data, status=status.HTTP_201_CREATED, ) + + +router = DefaultRouter(trailing_slash=False) +router.register(r"comment", CommentViewSet) diff --git a/backend/coreapp/views/user.py b/backend/coreapp/views/user.py index 5934c9858..2e91f41bd 100644 --- a/backend/coreapp/views/user.py +++ b/backend/coreapp/views/user.py @@ -10,7 +10,9 @@ from ..models.github import GitHubUser from ..models.profile import Profile from ..models.scratch import Scratch -from ..serializers import TerseScratchSerializer, serialize_profile +from ..models.comment import Comment +from ..serializers import TerseScratchSerializer, serialize_profile, CommentSerializer +from .comment import CommentPagination from .scratch import ScratchPagination @@ -71,6 +73,30 @@ def get_queryset(self) -> QuerySet[Scratch]: return Scratch.objects.filter(owner__user__username=self.kwargs["username"]) +class CurrentUserCommentList(generics.ListAPIView): # type: ignore + """ + Gets the current user's comments + """ + + pagination_class = CommentPagination + serializer_class = CommentSerializer + + def get_queryset(self) -> QuerySet[Scratch]: + return Comment.objects.filter(owner=self.request.profile) + + +class UserCommentList(generics.ListAPIView): # type: ignore + """ + Gets a user's comments + """ + + pagination_class = CommentPagination + serializer_class = CommentSerializer + + def get_queryset(self) -> QuerySet[Scratch]: + return Comment.objects.filter(owner__user__username=self.kwargs["username"]) + + @api_view(["GET"]) # type: ignore def user(request: Request, username: str) -> Response: """ diff --git a/frontend/src/components/Scratch/CommentsPanel.module.scss b/frontend/src/components/Scratch/Comments.module.scss similarity index 100% rename from frontend/src/components/Scratch/CommentsPanel.module.scss rename to frontend/src/components/Scratch/Comments.module.scss diff --git a/frontend/src/components/Scratch/Comments.tsx b/frontend/src/components/Scratch/Comments.tsx new file mode 100644 index 000000000..2db93581f --- /dev/null +++ b/frontend/src/components/Scratch/Comments.tsx @@ -0,0 +1,59 @@ + +import useSWR from "swr"; + +import { useState } from "react"; + +import { TerseScratch, get } from "@/lib/api"; +import * as api from "@/lib/api" +import AsyncButton from "../AsyncButton" +import { Comment } from "@/lib/api"; +import { styles } from "./Comments.module.scss" + + + + +export default function Comments({ scratch }: { scratch: TerseScratch }) { + const { results, _isLoading, hasNext, loadNext } = api.usePaginated(`/comment?scratch=${scratch.slug}&page_size=10`) + const [text, setText] = useState("") + const submit = async () => { + try { + const comment: api.Comment = await api.post("/comment", { + text: text, + + }) + } catch (error) { + console.error(error) + throw error + } + } + + + console.log(text) + return ( +
+
+ {results.map((comment: Comment) => { + return ( +
+

{comment.owner.username}:

+

{comment.text}

+
+
+ ) + }) + } + { hasNext &&
  • + + Show more + +
  • } +
    +
    +
    + { console.log(e.target.value); setText(e.target.value) }} /> + Submit Comment +
    +
    +
    + ) +} diff --git a/frontend/src/components/Scratch/CommentsPanel.tsx b/frontend/src/components/Scratch/CommentsPanel.tsx index eab92ccb9..da6998725 100644 --- a/frontend/src/components/Scratch/CommentsPanel.tsx +++ b/frontend/src/components/Scratch/CommentsPanel.tsx @@ -1,14 +1,25 @@ +import dynamic from "next/dynamic" + import { TerseScratch } from "@/lib/api" +import Loading from "@/components/loading.svg" import { styles } from "./CommentsPanel.module.scss" type Props = { scratch: TerseScratch } +const Comments = dynamic(() => import("./Comments"), { + loading: () =>
    + +
    , +}) + + -export default function CommentsPanel({ scratch }): Props { +export default function CommentsPanel({ scratch }: Props) { return (
    +
    ) } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ebc5e6a3b..8fbe6c8ea 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -301,6 +301,19 @@ export function usePreset(id: number | undefined): Preset | undefined { return data } +export function useComments(scratch: Scratch): Comment | undefined { + const url = typeof scratch.slug === "string" ? `/comment?scratch=${scratch.slug}` : null + const { data } = useSWR(url, get, { + refreshInterval: 0, + onErrorRetry, + }) + return data +} + +export function claimComment(comment: Comment): { + +} + export function usePaginated(url: string, firstPage?: Page): { results: T[] hasNext: boolean diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 2b36bc7fd..aa2ca7fcd 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -156,6 +156,13 @@ export interface Platform extends PlatformBase { presets: Preset[] } +export interface Comment { + slug: string + text: string + owner: User | null + creation_time: string +} + export function isAnonUser(user: User | AnonymousUser): user is AnonymousUser { return user.is_anonymous } From 1b3188574a8ced0a90280cdf8b3fbd16286fe1aa Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Fri, 10 May 2024 11:59:20 -0400 Subject: [PATCH 04/16] Post comment working --- backend/coreapp/serializers.py | 15 +++++++------ backend/coreapp/views/comment.py | 22 +++++++++++++++++--- frontend/src/components/Scratch/Comments.tsx | 11 +++------- frontend/src/lib/api.ts | 4 ---- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/backend/coreapp/serializers.py b/backend/coreapp/serializers.py index 17064108d..413f8effe 100644 --- a/backend/coreapp/serializers.py +++ b/backend/coreapp/serializers.py @@ -321,7 +321,6 @@ class Meta: class CommentSerializer(serializers.ModelSerializer[Comment]): owner = ProfileField(read_only=True) - text = serializers.CharField(allow_blank=True, trim_whitespace=False) class Meta: model = Comment @@ -334,12 +333,12 @@ class Meta: ] def create(self, validated_data: Any) -> Comment: - comment = Project.objects.create(**validated_data) + comment = Comment.objects.create(**validated_data) return comment - def validate(self, data: Dict[str, Any]) -> Dict[str, Any]: - """ - TODO: Validate that the scratch and the user are both valid to allow comment - creation - """ - return True + # def validate(self, data: Dict[str, Any]) -> Dict[str, Any]: + # """ + # TODO: Validate that the scratch and the user are both valid to allow comment + # creation + # """ + # return True diff --git a/backend/coreapp/views/comment.py b/backend/coreapp/views/comment.py index 78d29c0e9..1bbcc04b5 100644 --- a/backend/coreapp/views/comment.py +++ b/backend/coreapp/views/comment.py @@ -9,10 +9,11 @@ from rest_framework.request import Request from rest_framework.exceptions import APIException from rest_framework.routers import DefaultRouter -# from django.http import HttpResponseForbidden -# from ..models.scratch import Scratch + from ..models.comment import Comment from ..models.github import GitHubUser +from ..models.profile import Profile +from ..models.scratch import Scratch from ..serializers import CommentSerializer from django.contrib.auth.models import User @@ -22,6 +23,11 @@ class GithubLoginException(APIException): default_detail = "You must be logged in to Github to perform this action." +class ScratchSlugException(APIException): + status_code = status.HTTP_403_FORBIDDEN + default_detail = "Invalid Scratch Slug" + + class CommentPagination(CursorPagination): ordering = "-creation_time" page_size = 50 @@ -48,17 +54,27 @@ class CommentViewSet( def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: user: Optional[User] = request.profile.user + if not user: raise GithubLoginException() gh_user: Optional[GitHubUser] = user.github if not gh_user: raise GithubLoginException() + profile: Profile = Profile.objects.get(user=request.profile.user) + if not profile: + raise GithubLoginException() + scratch = Scratch.objects.get(slug=request.GET.get('scratch_id')) + if not scratch: + raise ScratchSlugException() serializer = CommentSerializer(data=request.data) serializer.is_valid(raise_exception=True) - comment = serializer.save() + comment.owner = profile + comment.scratch = scratch + comment.save() + return Response( CommentSerializer(comment, context={"request": request}).data, status=status.HTTP_201_CREATED, diff --git a/frontend/src/components/Scratch/Comments.tsx b/frontend/src/components/Scratch/Comments.tsx index 2db93581f..ce1ddf2ed 100644 --- a/frontend/src/components/Scratch/Comments.tsx +++ b/frontend/src/components/Scratch/Comments.tsx @@ -17,24 +17,19 @@ export default function Comments({ scratch }: { scratch: TerseScratch }) { const [text, setText] = useState("") const submit = async () => { try { - const comment: api.Comment = await api.post("/comment", { - text: text, - - }) + await api.post(`/comment?scratch_id=${scratch.slug}`, {text: text}) } catch (error) { console.error(error) throw error } } - - console.log(text) return (
    {results.map((comment: Comment) => { return ( -
    +

    {comment.owner.username}:

    {comment.text}


    @@ -50,7 +45,7 @@ export default function Comments({ scratch }: { scratch: TerseScratch }) {
    - { console.log(e.target.value); setText(e.target.value) }} /> + {setText(e.target.value) }} /> Submit Comment
    diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 8fbe6c8ea..d2d424d6e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -310,10 +310,6 @@ export function useComments(scratch: Scratch): Comment | undefined { return data } -export function claimComment(comment: Comment): { - -} - export function usePaginated(url: string, firstPage?: Page): { results: T[] hasNext: boolean From 6dbac4d6ee86412e60320c18d105778ac953707f Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Fri, 10 May 2024 19:08:46 -0400 Subject: [PATCH 05/16] Added date-fns --- frontend/package.json | 1 + frontend/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index e5717afeb..2072d5bf4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "ansi-to-react": "^6.1.6", "classnames": "^2.5.1", "codemirror": "^6.0.1", + "date-fns": "^3.6.0", "dotenv": "^16.4.5", "downshift": "9.0.0", "fast-myers-diff": "^3.2.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 4e39d9d08..bf3a3e735 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2871,6 +2871,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +date-fns@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + debounce@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" From bef486cfa2f4a20d6351cac3c3b817ce8a25403b Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Fri, 10 May 2024 21:03:05 -0400 Subject: [PATCH 06/16] Replaced TimeAgo with date-fns --- frontend/package.json | 1 - frontend/src/components/ScratchList.tsx | 10 +++++----- frontend/yarn.lock | 5 ----- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 2072d5bf4..9d1e43666 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,6 @@ "react-contenteditable": "^3.3.7", "react-dom": "^18.2.0", "react-laag": "^2.0.5", - "react-timeago": "^7.2.0", "react-virtualized-auto-sizer": "^1.0.24", "react-window": "^1.8.10", "sass": "^1.72.0", diff --git a/frontend/src/components/ScratchList.tsx b/frontend/src/components/ScratchList.tsx index ffea13bb3..9f3c29ae5 100644 --- a/frontend/src/components/ScratchList.tsx +++ b/frontend/src/components/ScratchList.tsx @@ -5,7 +5,7 @@ import { ReactNode, useState } from "react" import Link from "next/link" import classNames from "classnames" -import TimeAgo from "react-timeago" +import { formatDistanceToNowStrict } from 'date-fns' import * as api from "@/lib/api" import { presetUrl, scratchUrl } from "@/lib/api/urls" @@ -111,7 +111,7 @@ export function ScratchItem({ scratch, children }: { scratch: api.TerseScratch,
    - {presetOrCompiler} • {matchPercentString} matched • + {presetOrCompiler} • {matchPercentString} matched • {formatDistanceToNowStrict(scratch.last_updated)} ago
    {children} @@ -148,7 +148,7 @@ export function ScratchItemNoOwner({ scratch }: { scratch: api.TerseScratch }) {
    - {presetOrCompiler} • {matchPercentString} matched • + {presetOrCompiler} • {matchPercentString} matched • {formatDistanceToNowStrict(scratch.last_updated)} ago
    @@ -186,7 +186,7 @@ export function ScratchItemPlatformList({ scratch }: { scratch: api.TerseScratch
    - {presetOrCompiler} • {matchPercentString} matched • + {presetOrCompiler} • {matchPercentString} matched • {formatDistanceToNowStrict(scratch.last_updated)} ago
    @@ -206,7 +206,7 @@ export function ScratchItemPresetList({ scratch }: { scratch: api.TerseScratch }
    - {matchPercentString} matched • + {matchPercentString} matched • {formatDistanceToNowStrict(scratch.last_updated)} ago
    diff --git a/frontend/yarn.lock b/frontend/yarn.lock index bf3a3e735..db741156f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5514,11 +5514,6 @@ react-laag@^2.0.5: dependencies: tiny-warning "^1.0.3" -react-timeago@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/react-timeago/-/react-timeago-7.2.0.tgz#ae929d7423a63cbc3dc228e49d22fbf586d459ca" - integrity sha512-2KsBEEs+qRhKx/kekUVNSTIpop3Jwd7SRBm0R4Eiq3mPeswRGSsftY9FpKsE/lXLdURyQFiHeHFrIUxLYskG5g== - react-virtualized-auto-sizer@^1.0.24: version "1.0.24" resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz#3ebdc92f4b05ad65693b3cc8e7d8dd54924c0227" From e3bce916feee172605f73bbe5bcf9e72dd0fb55e Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Fri, 10 May 2024 21:13:39 -0400 Subject: [PATCH 07/16] Replaced TimeAgo with date-fns --- frontend/src/components/Scratch/AboutScratch.tsx | 6 +++--- frontend/src/components/Scratch/ScratchToolbar.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Scratch/AboutScratch.tsx b/frontend/src/components/Scratch/AboutScratch.tsx index b318440d2..b125a8128 100644 --- a/frontend/src/components/Scratch/AboutScratch.tsx +++ b/frontend/src/components/Scratch/AboutScratch.tsx @@ -1,6 +1,5 @@ import Link from "next/link" -import TimeAgo from "react-timeago" import useSWR from "swr" import { Scratch, Preset, get, usePreset } from "@/lib/api" @@ -13,6 +12,7 @@ import { getScoreText } from "../ScoreBadge" import UserLink from "../user/UserLink" import styles from "./AboutScratch.module.scss" +import { formatDistanceToNowStrict } from "date-fns" function ScratchLink({ url }: { url: string }) { const { data: scratch, error } = useSWR(url, get) @@ -78,11 +78,11 @@ export default function AboutScratch({ scratch, setScratch }: Props) {
    }

    Created

    - + {formatDistanceToNowStrict(scratch.last_updated)} ago

    Modified

    - + {formatDistanceToNowStrict(scratch.last_updated)} ago
    diff --git a/frontend/src/components/Scratch/ScratchToolbar.tsx b/frontend/src/components/Scratch/ScratchToolbar.tsx index 05214a8fe..cb45e9840 100644 --- a/frontend/src/components/Scratch/ScratchToolbar.tsx +++ b/frontend/src/components/Scratch/ScratchToolbar.tsx @@ -5,7 +5,6 @@ import Link from "next/link" import { DownloadIcon, FileIcon, IterationsIcon, RepoForkedIcon, SyncIcon, TrashIcon, UploadIcon } from "@primer/octicons-react" import classNames from "classnames" import ContentEditable from "react-contenteditable" -import TimeAgo from "react-timeago" import * as api from "@/lib/api" import { scratchUrl } from "@/lib/api/urls" @@ -19,6 +18,7 @@ import UserAvatar from "../user/UserAvatar" import useFuzzySaveCallback, { FuzzySaveAction } from "./hooks/useFuzzySaveCallback" import styles from "./ScratchToolbar.module.scss" +import { formatDistanceToNowStrict } from "date-fns" const ACTIVE_MS = 1000 * 60 @@ -57,7 +57,7 @@ function EditTimeAgo({ date }: { date: string }) { {isActive ? <> Active now : <> - + {formatDistanceToNowStrict(date)} ago } } From 3c9c1b75b42022aef35da5bba99fb490b000b69c Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Fri, 10 May 2024 21:15:26 -0400 Subject: [PATCH 08/16] Refined comments --- backend/coreapp/models/comment.py | 2 +- backend/coreapp/views/comment.py | 1 - .../components/Scratch/Comments.module.scss | 16 +++++++ frontend/src/components/Scratch/Comments.tsx | 46 +++++++++++++------ .../src/components/Scratch/CommentsPanel.tsx | 1 - 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/backend/coreapp/models/comment.py b/backend/coreapp/models/comment.py index 9c55e6e99..5b8c6a7b2 100644 --- a/backend/coreapp/models/comment.py +++ b/backend/coreapp/models/comment.py @@ -23,4 +23,4 @@ class Meta: ordering = ["-creation_time"] def __str__(self) -> str: - return self.slug + return f'{self.creation_time} - {self.owner} - {self.scratch} - {self.text[:50]}' diff --git a/backend/coreapp/views/comment.py b/backend/coreapp/views/comment.py index 1bbcc04b5..098b99c83 100644 --- a/backend/coreapp/views/comment.py +++ b/backend/coreapp/views/comment.py @@ -32,7 +32,6 @@ class CommentPagination(CursorPagination): ordering = "-creation_time" page_size = 50 page_size_query_param = "page_size" - max_page_size = 100 class CommentViewSet( diff --git a/frontend/src/components/Scratch/Comments.module.scss b/frontend/src/components/Scratch/Comments.module.scss index e69de29bb..1e084b130 100644 --- a/frontend/src/components/Scratch/Comments.module.scss +++ b/frontend/src/components/Scratch/Comments.module.scss @@ -0,0 +1,16 @@ +.textInput { + width: 100%; + padding: 8px 10px; + + color: var(--g1200); + background: var(--g200); + font: 0.8rem var(--monospace); + border: 1px solid var(--g500); + border-radius: 4px; + + outline: none !important; + + &::-webkit-input-placeholder { + color: var(--g700); + } +} diff --git a/frontend/src/components/Scratch/Comments.tsx b/frontend/src/components/Scratch/Comments.tsx index ce1ddf2ed..b4f5b17f1 100644 --- a/frontend/src/components/Scratch/Comments.tsx +++ b/frontend/src/components/Scratch/Comments.tsx @@ -1,51 +1,69 @@ +import { useEffect, useRef, useState } from "react"; -import useSWR from "swr"; - -import { useState } from "react"; - +import Loading from "@/components/loading.svg" import { TerseScratch, get } from "@/lib/api"; import * as api from "@/lib/api" import AsyncButton from "../AsyncButton" import { Comment } from "@/lib/api"; -import { styles } from "./Comments.module.scss" +import UserLink from "../user/UserLink"; +import styles from "./Comments.module.scss" export default function Comments({ scratch }: { scratch: TerseScratch }) { - const { results, _isLoading, hasNext, loadNext } = api.usePaginated(`/comment?scratch=${scratch.slug}&page_size=10`) + const { results, isLoading, hasNext, loadNext } = api.usePaginated(`/comment?scratch=${scratch.slug}&page_size=10`) const [text, setText] = useState("") + const bottomRef = useRef(null) + const submit = async () => { try { - await api.post(`/comment?scratch_id=${scratch.slug}`, {text: text}) + const reponse = await api.post(`/comment?scratch_id=${scratch.slug}`, { text: text }) + console.log(reponse) + setText("") } catch (error) { console.error(error) throw error } } + + const scrollToBottom = () => { + console.log("Scrolling") + bottomRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" }) + } + + useEffect(() => { + if (bottomRef.current) { + scrollToBottom() + } + }, [bottomRef]) + return ( -
    -
    +
    + {isLoading &&
    + +
    } +
    {results.map((comment: Comment) => { return (
    -

    {comment.owner.username}:

    +

    -

    {comment.text}


    ) }) } - { hasNext &&
  • + {hasNext &&
  • Show more
  • }
    -
    -
    - {setText(e.target.value) }} /> +
    +
    + { setText(e.target.value) }} /> Submit Comment
    diff --git a/frontend/src/components/Scratch/CommentsPanel.tsx b/frontend/src/components/Scratch/CommentsPanel.tsx index da6998725..79a3ca53e 100644 --- a/frontend/src/components/Scratch/CommentsPanel.tsx +++ b/frontend/src/components/Scratch/CommentsPanel.tsx @@ -3,7 +3,6 @@ import dynamic from "next/dynamic" import { TerseScratch } from "@/lib/api" import Loading from "@/components/loading.svg" -import { styles } from "./CommentsPanel.module.scss" type Props = { scratch: TerseScratch From 599fd06016378c2eb459fc3771cccbecbd092a08 Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Fri, 10 May 2024 21:44:26 -0400 Subject: [PATCH 09/16] Added Datetime and such --- .../migrations/0055_alter_comment_options.py | 16 ++++++++++++++ .../components/Scratch/Comments.module.scss | 10 +++++++++ frontend/src/components/Scratch/Comments.tsx | 22 ++++++++++++++----- 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 backend/coreapp/migrations/0055_alter_comment_options.py diff --git a/backend/coreapp/migrations/0055_alter_comment_options.py b/backend/coreapp/migrations/0055_alter_comment_options.py new file mode 100644 index 000000000..63b12fb85 --- /dev/null +++ b/backend/coreapp/migrations/0055_alter_comment_options.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.3 on 2024-05-10 21:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("coreapp", "0054_comment"), + ] + + operations = [ + migrations.AlterModelOptions( + name="comment", + options={"ordering": ["creation_time"]}, + ), + ] diff --git a/frontend/src/components/Scratch/Comments.module.scss b/frontend/src/components/Scratch/Comments.module.scss index 1e084b130..fc763816f 100644 --- a/frontend/src/components/Scratch/Comments.module.scss +++ b/frontend/src/components/Scratch/Comments.module.scss @@ -14,3 +14,13 @@ color: var(--g700); } } + +.metadata { + display: flex; + padding-left: 5px; + color: var(--g900); + + > span { + flex-grow: 1; + } +} diff --git a/frontend/src/components/Scratch/Comments.tsx b/frontend/src/components/Scratch/Comments.tsx index b4f5b17f1..d6429a7ca 100644 --- a/frontend/src/components/Scratch/Comments.tsx +++ b/frontend/src/components/Scratch/Comments.tsx @@ -6,6 +6,7 @@ import * as api from "@/lib/api" import AsyncButton from "../AsyncButton" import { Comment } from "@/lib/api"; import UserLink from "../user/UserLink"; +import { formatDistanceToNowStrict } from "date-fns" import styles from "./Comments.module.scss" @@ -15,12 +16,16 @@ export default function Comments({ scratch }: { scratch: TerseScratch }) { const { results, isLoading, hasNext, loadNext } = api.usePaginated(`/comment?scratch=${scratch.slug}&page_size=10`) const [text, setText] = useState("") const bottomRef = useRef(null) + const inputRef = useRef(null) const submit = async () => { try { - const reponse = await api.post(`/comment?scratch_id=${scratch.slug}`, { text: text }) - console.log(reponse) - setText("") + const response = await api.post(`/comment?scratch_id=${scratch.slug}`, { text: text }) + if (response) { + inputRef.current?.reset() + } + console.log(response) + } catch (error) { console.error(error) throw error @@ -47,8 +52,11 @@ export default function Comments({ scratch }: { scratch: TerseScratch }) {
    {results.map((comment: Comment) => { return ( -
    -

    -

    +
    +
    +

    + {formatDistanceToNowStrict(comment.creation_time)} ago +

    {comment.text}


    @@ -63,7 +71,9 @@ export default function Comments({ scratch }: { scratch: TerseScratch }) {
    - { setText(e.target.value) }} /> +
    + { setText(e.target.value) }} /> +
    Submit Comment
    From 1881f821ec0403d208842f585a91a4131188b81b Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Sat, 11 May 2024 16:44:51 -0400 Subject: [PATCH 10/16] Temp commit --- backend/coreapp/views/comment.py | 5 +- frontend/src/components/Scratch/Comments.tsx | 73 ++++++++++++++------ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/backend/coreapp/views/comment.py b/backend/coreapp/views/comment.py index 098b99c83..95080db46 100644 --- a/backend/coreapp/views/comment.py +++ b/backend/coreapp/views/comment.py @@ -24,7 +24,7 @@ class GithubLoginException(APIException): class ScratchSlugException(APIException): - status_code = status.HTTP_403_FORBIDDEN + status_code = status.HTTP_400_BAD_REQUEST default_detail = "Invalid Scratch Slug" @@ -44,11 +44,12 @@ class CommentViewSet( ): queryset = Comment.objects.all() pagination_class = CommentPagination - filterset_fields = ["scratch"] + filterset_fields = ["scratch", "slug"] filter_backends = [ django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter, ] + search_fields = ["scratch", "owner"] serializer_class = CommentSerializer def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: diff --git a/frontend/src/components/Scratch/Comments.tsx b/frontend/src/components/Scratch/Comments.tsx index d6429a7ca..0b333d471 100644 --- a/frontend/src/components/Scratch/Comments.tsx +++ b/frontend/src/components/Scratch/Comments.tsx @@ -7,37 +7,80 @@ import AsyncButton from "../AsyncButton" import { Comment } from "@/lib/api"; import UserLink from "../user/UserLink"; import { formatDistanceToNowStrict } from "date-fns" - import styles from "./Comments.module.scss" +import { createRoot } from 'react-dom/client'; + +interface ItemProps { + comment: Comment +} + +function CommentItem({ comment }: ItemProps) { + return ( +
    +
    +

    + {formatDistanceToNowStrict(comment.creation_time)} ago +
    +

    {comment.text}

    +
    +
    + ) +} +interface ListProps { + results: Comment[] +} + +function CommentList({ results }: ListProps) { + return ( +
    + {results.map((comment: Comment) => { + return + })} +
    + ) +} export default function Comments({ scratch }: { scratch: TerseScratch }) { const { results, isLoading, hasNext, loadNext } = api.usePaginated(`/comment?scratch=${scratch.slug}&page_size=10`) const [text, setText] = useState("") - const bottomRef = useRef(null) - const inputRef = useRef(null) - + const bottomRef = useRef(null) + const inputRef = useRef(null) + const commentListRef = useRef(null) + const root = useRef(null) const submit = async () => { try { const response = await api.post(`/comment?scratch_id=${scratch.slug}`, { text: text }) if (response) { + await api.get(`/comment?slug=${response.slug}`).then((comment) => { + results.unshift(comment.results[0]) + root.current?.render() + }) inputRef.current?.reset() } - console.log(response) - } catch (error) { console.error(error) throw error } } - const scrollToBottom = () => { console.log("Scrolling") bottomRef.current.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" }) } + useEffect(() => { + // Create the root if it doesn't exist + if (!root.current && commentListRef.current) { + root.current = createRoot(commentListRef.current); + } + return () => { + // Don't unmount the root here + // We'll keep the root throughout the component's lifecycle + }; + }, []); + useEffect(() => { if (bottomRef.current) { scrollToBottom() @@ -50,19 +93,9 @@ export default function Comments({ scratch }: { scratch: TerseScratch }) {
    }
    - {results.map((comment: Comment) => { - return ( -
    -
    -

    - {formatDistanceToNowStrict(comment.creation_time)} ago -
    -

    {comment.text}

    -
    -
    - ) - }) - } +
    + +
    {hasNext &&
  • Show more From 2b6f59972b82ff91e8664a8d48abfa98b20e532c Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Tue, 14 May 2024 09:13:42 -0400 Subject: [PATCH 11/16] t --- frontend/src/components/Scratch/Comments.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Scratch/Comments.tsx b/frontend/src/components/Scratch/Comments.tsx index 0b333d471..b78ba8389 100644 --- a/frontend/src/components/Scratch/Comments.tsx +++ b/frontend/src/components/Scratch/Comments.tsx @@ -87,11 +87,19 @@ export default function Comments({ scratch }: { scratch: TerseScratch }) { } }, [bottomRef]) - return ( -
    + if (isLoading) { + return (<> {isLoading &&
    -
    } +
    + } + + ) + } + + return ( +
    +
    From 02ec2c4fc1fc1b1c71916fbe7d80885b42b4dfc2 Mon Sep 17 00:00:00 2001 From: Conor Golden Date: Thu, 16 May 2024 00:09:20 -0400 Subject: [PATCH 12/16] * Added Dropdown comp * Added refined textarea * Added delete and edit comments --- frontend/src/components/Dropdown.module.scss | 53 ++++ frontend/src/components/Dropdown.tsx | 67 +++++ .../components/Scratch/Comments.module.scss | 75 ++++- frontend/src/components/Scratch/Comments.tsx | 273 +++++++++++++----- .../src/components/Scratch/CommentsPanel.tsx | 6 +- frontend/src/lib/api/urls.ts | 6 +- 6 files changed, 399 insertions(+), 81 deletions(-) create mode 100644 frontend/src/components/Dropdown.module.scss create mode 100644 frontend/src/components/Dropdown.tsx diff --git a/frontend/src/components/Dropdown.module.scss b/frontend/src/components/Dropdown.module.scss new file mode 100644 index 000000000..c0eb23521 --- /dev/null +++ b/frontend/src/components/Dropdown.module.scss @@ -0,0 +1,53 @@ + +.options { + display: none; + color: var(--g1600); + background: var(--g200); + position: absolute; + overflow-y: auto; + max-height: 320px; + z-index: 20; + width: max-content; + border-radius: 10px; + + &.open { + display: block; + } + + &.open:empty { + display: none; + } + + .option { + color: var(--g1600); + background: var(--g200); + border-radius: 5px; + + width: 100%; + } + + .option:hover { + background: var(--g1600); + color: var(--g200); + + } +} + +.options::-webkit-scrollbar { + width: 6px; +} + +.options::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px var(--g200); + border-radius: 10px; +} + +.options::-webkit-scrollbar-thumb { + background: var(--g1600); + border-radius: 10px; +} + +.options::-webkit-scrollbar-thumb:hover { + background: gray; +} + diff --git a/frontend/src/components/Dropdown.tsx b/frontend/src/components/Dropdown.tsx new file mode 100644 index 000000000..168b97905 --- /dev/null +++ b/frontend/src/components/Dropdown.tsx @@ -0,0 +1,67 @@ +import { ReactNode, useEffect, useRef, useState } from "react" + +import { ChevronDownIcon } from "@primer/octicons-react" +import classNames from "classnames" + +import styles from "./Dropdown.module.scss" + +export type Props = { + options: { [key: string]: (event: any) => void } + className?: string + children: ReactNode +} + +const useClickOutside = (ref, handler) => { + +}; + +export default function Dropdown({ options, children, className }: Props) { + const [isOpen, setIsOpen] = useState(false) + const ref = useRef(null) + + const toggleDropdown = () => { + setIsOpen(!isOpen); + }; + + const closeDropdown = () => { + setIsOpen(false); + }; + + useEffect(() => { + const listener = (event) => { + if (!ref?.current || ref.current.contains(event.target)) { + return; + } + closeDropdown(); + }; + + document.addEventListener('mousedown', listener); + document.addEventListener('touchstart', listener); + return () => { + document.removeEventListener('mousedown', listener); + document.addEventListener('touchstart', listener); + }; + }, [closeDropdown, ref]); + + return ( +
    + +
    + {Object.entries(options).map(([value, onChange]) => + + )} +
    +
    + ) +} diff --git a/frontend/src/components/Scratch/Comments.module.scss b/frontend/src/components/Scratch/Comments.module.scss index fc763816f..8825cbb44 100644 --- a/frontend/src/components/Scratch/Comments.module.scss +++ b/frontend/src/components/Scratch/Comments.module.scss @@ -1,18 +1,53 @@ -.textInput { +.holder { + height: 100%; + position: relative; +} + +.inputSection { width: 100%; - padding: 8px 10px; + position: absolute; + left: 0; + bottom: 0; + display: contents; - color: var(--g1200); - background: var(--g200); - font: 0.8rem var(--monospace); - border: 1px solid var(--g500); - border-radius: 4px; + form { + width: 100%; - outline: none !important; + textarea { + appearance: none; + width: 100%; + padding: 8px 10px; + height: 33px; + color: var(--g1200); + background: var(--g200); + font: 0.8rem var(--monospace); + border: 1px solid var(--g500); + border-radius: 4px; + bottom: 0; + left: 0; + padding-right: 2em; + outline: none !important; + overflow: hidden; + resize: none; - &::-webkit-input-placeholder { - color: var(--g700); + &::-webkit-input-placeholder { + color: var(--g700); + } + } } + + .submit { + position: absolute; + border: none; + transform: translateY(-15%); + right: 0; + bottom: 0; + } +} + +.commentsBody { + display: flex; + flex-direction: column-reverse; } .metadata { @@ -20,7 +55,25 @@ padding-left: 5px; color: var(--g900); - > span { + >span { flex-grow: 1; } } + +.text { + overflow-wrap: break-word; + white-space: pre-wrap; +} + +.refresh { + position: fixed; + z-index: 2; +} + +.counter { + float: right; + line-height: 1.25rem; + padding-bottom: 4px; + font: 0.8rem var(--monospace); + color: var(--g1200); +} diff --git a/frontend/src/components/Scratch/Comments.tsx b/frontend/src/components/Scratch/Comments.tsx index b78ba8389..aa380403b 100644 --- a/frontend/src/components/Scratch/Comments.tsx +++ b/frontend/src/components/Scratch/Comments.tsx @@ -1,91 +1,214 @@ -import { useEffect, useRef, useState } from "react"; +"use client" + +import { ChangeEvent, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; +import useSWR, { mutate } from "swr"; -import Loading from "@/components/loading.svg" -import { TerseScratch, get } from "@/lib/api"; import * as api from "@/lib/api" -import AsyncButton from "../AsyncButton" import { Comment } from "@/lib/api"; -import UserLink from "../user/UserLink"; +import { commentUrl } from "@/lib/api/urls"; + import { formatDistanceToNowStrict } from "date-fns" + +import { ArrowUpIcon, KebabHorizontalIcon, CheckIcon } from "@primer/octicons-react" +import Loading from "@/components/loading.svg" + +import AsyncButton from "../AsyncButton" +import UserLink from "../user/UserLink"; +import Button from "../Button"; + import styles from "./Comments.module.scss" -import { createRoot } from 'react-dom/client'; +import Dropdown from "../Dropdown"; -interface ItemProps { - comment: Comment +const maxTextLength = 5000 + +async function deleteComment(comment: Comment) { + try { + await api.delete_(commentUrl(comment), {}) + alert("Comment deleted") + } catch (e) { + alert(`Error Deleting Comment: ${e}`) + } } -function CommentItem({ comment }: ItemProps) { +function EditComment({ comment, stopEditing, submit }: { comment: any, stopEditing: () => void, submit: (text: string) => Promise }) { + const [text, setText] = useState(comment.text) + const textareaRef = useRef(null); + + const handleInput = (e: ChangeEvent) => { + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + textareaRef.current.style.height = `${e.target.scrollHeight + 2}px`; + } + setText(e.target.value) + }; + return (
    -
    +

    - {formatDistanceToNowStrict(comment.creation_time)} ago
    -

    {comment.text}

    +
    +
    +
    +
    +