From 67037fec37a16e84cd83eece65b4677f15df6a64 Mon Sep 17 00:00:00 2001 From: Karen Gao <48370412+karengao6@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:54:19 -0500 Subject: [PATCH 01/14] DEV-521 Add Karen as Contributor (#18) --- frontend/app/about/page.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/app/about/page.tsx b/frontend/app/about/page.tsx index e74d9a9..b6f574f 100644 --- a/frontend/app/about/page.tsx +++ b/frontend/app/about/page.tsx @@ -76,6 +76,14 @@ const teamMembers = [ linkedin: 'https://www.linkedin.com/in/angcai/', }, }, + { + name: 'Karen Gao', + role: 'Software Engineer', + imgSrc: 'https://media.licdn.com/dms/image/v2/D5603AQH24u_lw8Ix2A/profile-displayphoto-shrink_400_400/profile-displayphoto-shrink_400_400/0/1727058129445?e=1773273600&v=beta&t=YPDY4mzM53X5QZaJI-8WmyAk32orQdwCMz7bBohkjgY', + socials: { + linkedin: 'https://www.linkedin.com/in/karen-a-gao/', + }, + }, ]; /** From 60bcb2a38ec529bd6a3552447e3c2aa0141a4dc7 Mon Sep 17 00:00:00 2001 From: Angela Cai Date: Wed, 25 Feb 2026 17:20:51 -0500 Subject: [PATCH 02/14] DEV-581 Event Model Serializer (#16) * Added event model sterilizer * added error messages and field names * changed to like mail --- backend/hoagiecalendar/api/event_views.py | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/backend/hoagiecalendar/api/event_views.py b/backend/hoagiecalendar/api/event_views.py index 3b69474..4cbb704 100644 --- a/backend/hoagiecalendar/api/event_views.py +++ b/backend/hoagiecalendar/api/event_views.py @@ -1,6 +1,42 @@ from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework import serializers +from ..models.event import Event +class EventSerializer(serializers.ModelSerializer): + name = serializers.CharField( + max_length=100, + min_length=3, + error_messages={ + "required": "Name must be at least 3 characters.", + "min_length": "Name must be at least 3 characters.", + "max_length": "Name must be at most 100 characters.", + }, + ) + location = serializers.CharField( + max_length=100, + min_length=3, + error_messages={ + "required": "Location must be at least 3 characters.", + "min_length": "Location must be at least 3 characters.", + "max_length": "Location must be at most 100 characters.", + }, + ) + host = serializers.CharField( + max_length=100, + min_length=3, + error_messages={ + "required": "Host must be at least 3 characters.", + "min_length": "Host must be at least 3 characters.", + "max_length": "Host must be at most 100 characters.", + }, + ) + class Meta: + model = Event + fields = ['start', 'end', 'name', 'location', 'description', + 'host', 'owner', 'category', 'from_mail'] + read_only_fields = ['owner'] + class EventView(APIView): def get(self, request) -> Response: From cb1529392614e3d563cc67fbc4b23c57b161c283 Mon Sep 17 00:00:00 2001 From: Chloe Chen <133549470+celloii@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:21:00 -0500 Subject: [PATCH 03/14] DEV-603 Add Chloe Chen as contributer (#19) Co-authored-by: Jenny Fan <40123244+jfmath04@users.noreply.github.com> --- frontend/app/about/page.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/app/about/page.tsx b/frontend/app/about/page.tsx index b6f574f..44ba481 100644 --- a/frontend/app/about/page.tsx +++ b/frontend/app/about/page.tsx @@ -76,6 +76,14 @@ const teamMembers = [ linkedin: 'https://www.linkedin.com/in/angcai/', }, }, + { + name: 'Chloe Chen', + role: 'Software Engineer', + imgSrc: 'https://media.licdn.com/dms/image/v2/D4E03AQGtOBqymzIJ6Q/profile-displayphoto-scale_400_400/B4EZyIt1WtJsAg-/0/1771820246551?e=1773273600&v=beta&t=oZxsneIF-hkukaX0rNoV7tt6NcpmgjYWALc5l8MRzp8', + socials: { + linkedin: 'https://www.linkedin.com/in/chloe-chen-7388243b2', + }, + }, { name: 'Karen Gao', role: 'Software Engineer', From 26b242ce302e94681ddfadd5170fde9163df7cd2 Mon Sep 17 00:00:00 2001 From: Jenny Fan <40123244+jfmath04@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:41:16 -0500 Subject: [PATCH 04/14] DEV-639 Backend Lint Workflow (#22) * Add Ruff to dev dependencies and run linting * Linting workflow for backend --- .github/workflows/lint.yml | 86 +++++++++++++++-------- backend/hoagiecalendar/admin.py | 2 - backend/hoagiecalendar/api/event_views.py | 52 +++++++------- backend/hoagiecalendar/tests.py | 2 - backend/pyproject.toml | 5 ++ backend/uv.lock | 33 +++++++++ 6 files changed, 121 insertions(+), 59 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f756e54..6bf23b4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,33 +1,59 @@ name: Lint on: pull_request jobs: - run-linters: - name: Run linters - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./frontend - - steps: - - name: Check out Git repository - uses: actions/checkout@v5 - - - name: Set up Node.js - uses: actions/setup-node@v5 - with: - node-version: 22 - - - name: Enable Corepack - run: corepack enable - - # ESLint and Prettier must be in `package.json` - - name: Install Node.js dependencies - run: yarn install --immutable - - - name: Run linters - uses: wearerequired/lint-action@v2.3.0 - with: - eslint: true - eslint_dir: ./frontend - prettier: true - prettier_dir: ./frontend \ No newline at end of file + run-linters: + name: Run linters + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend + + steps: + - name: Check out Git repository + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Enable Corepack + run: corepack enable + + # ESLint and Prettier must be in `package.json` + - name: Install Node.js dependencies + run: yarn install --immutable + + - name: Run linters + uses: wearerequired/lint-action@v2.3.0 + with: + eslint: true + eslint_dir: ./frontend + prettier: true + prettier_dir: ./frontend + + ruff: + name: Ruff (backend) + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./backend + + steps: + - name: Check out Git repository + uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install + + - name: Install backend dependencies + run: uv sync --group dev + + - name: Run Ruff lint check + run: uv run ruff check + + - name: Run Ruff format check + run: uv run ruff format --check diff --git a/backend/hoagiecalendar/admin.py b/backend/hoagiecalendar/admin.py index 8c38f3f..846f6b4 100644 --- a/backend/hoagiecalendar/admin.py +++ b/backend/hoagiecalendar/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/backend/hoagiecalendar/api/event_views.py b/backend/hoagiecalendar/api/event_views.py index 4cbb704..9923a63 100644 --- a/backend/hoagiecalendar/api/event_views.py +++ b/backend/hoagiecalendar/api/event_views.py @@ -1,10 +1,12 @@ +from rest_framework import serializers from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework import serializers + from ..models.event import Event + class EventSerializer(serializers.ModelSerializer): - name = serializers.CharField( + name = serializers.CharField( max_length=100, min_length=3, error_messages={ @@ -13,7 +15,7 @@ class EventSerializer(serializers.ModelSerializer): "max_length": "Name must be at most 100 characters.", }, ) - location = serializers.CharField( + location = serializers.CharField( max_length=100, min_length=3, error_messages={ @@ -22,7 +24,7 @@ class EventSerializer(serializers.ModelSerializer): "max_length": "Location must be at most 100 characters.", }, ) - host = serializers.CharField( + host = serializers.CharField( max_length=100, min_length=3, error_messages={ @@ -31,32 +33,32 @@ class EventSerializer(serializers.ModelSerializer): "max_length": "Host must be at most 100 characters.", }, ) - class Meta: - model = Event - fields = ['start', 'end', 'name', 'location', 'description', - 'host', 'owner', 'category', 'from_mail'] - read_only_fields = ['owner'] - + + class Meta: + model = Event + fields = ["start", "end", "name", "location", "description", "host", "owner", "category", "from_mail"] + read_only_fields = ["owner"] + class EventView(APIView): - def get(self, request) -> Response: - # Logic to get events - pass + def get(self, request) -> Response: + # Logic to get events + pass - def post(self, request) -> Response: - # Logic to create an event - pass + def post(self, request) -> Response: + # Logic to create an event + pass class EventDetailView(APIView): - def get(self, request, event_id) -> Response: - # Logic to get details of an event - pass + def get(self, request, event_id) -> Response: + # Logic to get details of an event + pass - def put(self, request, event_id) -> Response: - # Logic to update details of an event - pass + def put(self, request, event_id) -> Response: + # Logic to update details of an event + pass - def delete(self, request, event_id) -> Response: - # Logic to delete an event - pass + def delete(self, request, event_id) -> Response: + # Logic to delete an event + pass diff --git a/backend/hoagiecalendar/tests.py b/backend/hoagiecalendar/tests.py index 7ce503c..a39b155 100644 --- a/backend/hoagiecalendar/tests.py +++ b/backend/hoagiecalendar/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6fc8562..7d0bc51 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -108,6 +108,11 @@ line-ending = "auto" # Automatically detect line endings requires = ["hatchling"] build-backend = "hatchling.build" +[dependency-groups] +dev = [ + "ruff>=0.15.4", +] + [tool.hatch.build.targets.wheel] packages = ["hoagiecalendar"] diff --git a/backend/uv.lock b/backend/uv.lock index 7b6c6a5..b661fd3 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -61,6 +61,11 @@ dependencies = [ { name = "python-dotenv" }, ] +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "dj-database-url", specifier = ">=3.1.0" }, @@ -70,6 +75,9 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.2.1" }, ] +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.15.4" }] + [[package]] name = "postgres" version = "4.0" @@ -145,6 +153,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "ruff" +version = "0.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, + { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, + { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, +] + [[package]] name = "sqlparse" version = "0.5.5" From bde63f4ead723c22615dd5abad1b50a6d860d56e Mon Sep 17 00:00:00 2001 From: Chloe Chen <133549470+celloii@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:13:01 -0400 Subject: [PATCH 05/14] DEV-582 Create Event Handler (#21) * post handler for event views * added check if serializer isvalid * simplified with raise exception parameter --- backend/hoagiecalendar/api/event_views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/hoagiecalendar/api/event_views.py b/backend/hoagiecalendar/api/event_views.py index 9923a63..02e7cea 100644 --- a/backend/hoagiecalendar/api/event_views.py +++ b/backend/hoagiecalendar/api/event_views.py @@ -1,4 +1,4 @@ -from rest_framework import serializers +from rest_framework import serializers, status from rest_framework.response import Response from rest_framework.views import APIView @@ -15,6 +15,7 @@ class EventSerializer(serializers.ModelSerializer): "max_length": "Name must be at most 100 characters.", }, ) + location = serializers.CharField( max_length=100, min_length=3, @@ -24,6 +25,7 @@ class EventSerializer(serializers.ModelSerializer): "max_length": "Location must be at most 100 characters.", }, ) + host = serializers.CharField( max_length=100, min_length=3, @@ -46,8 +48,11 @@ def get(self, request) -> Response: pass def post(self, request) -> Response: - # Logic to create an event - pass + serializer = EventSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + # saving the event with the user as the owner + serializer.save(owner=request.user) + return Response(serializer.data, status=status.HTTP_201_CREATED) class EventDetailView(APIView): From ccf15dd5fba207f0acd8ca98c246ce5dee23a889 Mon Sep 17 00:00:00 2001 From: Karen Gao <48370412+karengao6@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:51:11 -0400 Subject: [PATCH 06/14] DEV-583 Get Event Handler (#20) * complete the get() handler for EventView * implement filter by multiple categories in EventView * implement multiple category filter in EventView * implement multiple category filter in EventView * implement multiple category filter in EventView * change gt/lt to gte/lte, got rid of ruff in dependencies list in pyproject.toml * remake uv lock --- backend/hoagiecalendar/api/event_views.py | 42 +++++++++++++-- backend/hoagiecalendar/models/event.py | 3 ++ backend/uv.lock | 62 +++++++++++------------ 3 files changed, 72 insertions(+), 35 deletions(-) diff --git a/backend/hoagiecalendar/api/event_views.py b/backend/hoagiecalendar/api/event_views.py index 02e7cea..3a60392 100644 --- a/backend/hoagiecalendar/api/event_views.py +++ b/backend/hoagiecalendar/api/event_views.py @@ -38,14 +38,48 @@ class EventSerializer(serializers.ModelSerializer): class Meta: model = Event - fields = ["start", "end", "name", "location", "description", "host", "owner", "category", "from_mail"] - read_only_fields = ["owner"] + fields = [ + "start", + "end", + "name", + "location", + "description", + "host", + "owner", + "category", + "from_mail", + "ordering", + ] + read_only_fields = ["owner", "created_at", "updated_at"] class EventView(APIView): def get(self, request) -> Response: - # Logic to get events - pass + start_time = request.query_params.get("start_time") + end_time = request.query_params.get("end_time") + + # return an error if start_time or end_time are not in the request + if not start_time or not end_time: + return Response( + {"detail": "Both start_time and end_time query parameters are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # get all events that overlap with the given time interval + # (event end is after the start of the range, and event start is before the end of the range) + queryset = Event.objects.filter(end__gte=start_time, start__lte=end_time) + + # filter by matching category IDs (allow multiple categories) + category_ids = request.query_params.getlist("category_id") + if category_ids: + queryset = queryset.filter(category__id__in=category_ids) + + # make sure events returned are unique (in case of multiple category matches) + queryset = queryset.distinct() + + serializer = EventSerializer(queryset, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) def post(self, request) -> Response: serializer = EventSerializer(data=request.data) diff --git a/backend/hoagiecalendar/models/event.py b/backend/hoagiecalendar/models/event.py index 568c23c..1abcfe4 100644 --- a/backend/hoagiecalendar/models/event.py +++ b/backend/hoagiecalendar/models/event.py @@ -23,9 +23,12 @@ class Event(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE) category = models.ManyToManyField(Category, related_name="events", blank=True) from_mail = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) def __str__(self) -> str: return self.name class Meta: db_table = "Event" + ordering = ["start"] diff --git a/backend/uv.lock b/backend/uv.lock index b661fd3..e896c84 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -13,40 +13,40 @@ wheels = [ [[package]] name = "dj-database-url" -version = "3.1.0" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/c6/88676a7333fb7c668e626b55f8bfc8527dd863973eb1c40412b95d27747d/dj_database_url-3.1.0.tar.gz", hash = "sha256:d80218426b83f9302c8d27d4fccf52de5cf0cab179f0645fb2839f37605d1353", size = 7924, upload-time = "2026-01-04T09:18:32.693Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/f6/00b625e9d371b980aa261011d0dc906a16444cb688f94215e0dc86996eb5/dj_database_url-3.1.2.tar.gz", hash = "sha256:63c20e4bbaa51690dfd4c8d189521f6bf6bc9da9fcdb23d95d2ee8ee87f9ec62", size = 11490, upload-time = "2026-02-19T15:30:23.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e84f7472ab0bdacc3fd09556eb4dd40d88246941d465cc103b36a8dabcd8/dj_database_url-3.1.0-py3-none-any.whl", hash = "sha256:155a56fbbecbaaf1348ccd73bf29138b4c9988363ba08261a0f0145e392e638c", size = 8849, upload-time = "2026-01-04T09:18:43.77Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/57c66006373381f1d3e5bd94216f1d371228a89f443d3030e010f73dd198/dj_database_url-3.1.2-py3-none-any.whl", hash = "sha256:544e015fee3efa5127a1eb1cca465f4ace578265b3671fe61d0ed7dbafb5ec8a", size = 8953, upload-time = "2026-02-19T15:30:39.37Z" }, ] [[package]] name = "django" -version = "6.0.2" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/3e/a1c4207c5dea4697b7a3387e26584919ba987d8f9320f59dc0b5c557a4eb/django-6.0.2.tar.gz", hash = "sha256:3046a53b0e40d4b676c3b774c73411d7184ae2745fe8ce5e45c0f33d3ddb71a7", size = 10886874, upload-time = "2026-02-03T13:50:31.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/e1/894115c6bd70e2c8b66b0c40a3c367d83a5a48c034a4d904d31b62f7c53a/django-6.0.3.tar.gz", hash = "sha256:90be765ee756af8a6cbd6693e56452404b5ad15294f4d5e40c0a55a0f4870fe1", size = 10872701, upload-time = "2026-03-03T13:55:15.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/23f2556967c45e34d3d3cf032eb1bd3ef925ee458667fb99052a0b3ea3a6/django-6.0.3-py3-none-any.whl", hash = "sha256:2e5974441491ddb34c3f13d5e7a9f97b07ba03bf70234c0a9c68b79bbb235bc3", size = 8358527, upload-time = "2026-03-03T13:55:10.552Z" }, ] [[package]] name = "djangorestframework" -version = "3.16.1" +version = "3.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/46/615ad5425c7b941e6fd3f382d25b1aca680022200d8e5b87bfc665c4425d/djangorestframework-3.17.0.tar.gz", hash = "sha256:456fd992a33f9e64c9d0f47e85d9787db0efb44f894c1e513315b5e74765bd4c", size = 905671, upload-time = "2026-03-18T20:33:42.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/01dfe0e712d37007a4417bf7ba518a9e6830d4de009ed4bfdfebd45b99cf/djangorestframework-3.17.0-py3-none-any.whl", hash = "sha256:d84fe85f30b7ac6e8c0076ce9ff635e4eaedca5912f8d7d2926ce448c08533ba", size = 898818, upload-time = "2026-03-18T20:33:41.335Z" }, ] [[package]] @@ -146,36 +146,36 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "ruff" -version = "0.15.4" +version = "0.15.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, - { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, - { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, - { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, - { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, - { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, ] [[package]] From 56100510ba27ebff8c9c2054b129c5f3f71a148c Mon Sep 17 00:00:00 2001 From: Angela Cai Date: Mon, 23 Mar 2026 14:38:48 -0400 Subject: [PATCH 07/14] DEV-594 Get Event Detail Handler (#23) * Added event model sterilizer * added error messages and field names * changed to like mail * add id to read only * get put delete * ruff * id in normal field, delete delete * format * required to blank --------- Co-authored-by: Jenny Fan <40123244+jfmath04@users.noreply.github.com> --- backend/hoagiecalendar/api/event_views.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/hoagiecalendar/api/event_views.py b/backend/hoagiecalendar/api/event_views.py index 3a60392..02ab148 100644 --- a/backend/hoagiecalendar/api/event_views.py +++ b/backend/hoagiecalendar/api/event_views.py @@ -1,3 +1,4 @@ +from django.shortcuts import get_object_or_404 from rest_framework import serializers, status from rest_framework.response import Response from rest_framework.views import APIView @@ -10,7 +11,7 @@ class EventSerializer(serializers.ModelSerializer): max_length=100, min_length=3, error_messages={ - "required": "Name must be at least 3 characters.", + "blank": "Name must be at least 3 characters.", "min_length": "Name must be at least 3 characters.", "max_length": "Name must be at most 100 characters.", }, @@ -20,7 +21,7 @@ class EventSerializer(serializers.ModelSerializer): max_length=100, min_length=3, error_messages={ - "required": "Location must be at least 3 characters.", + "blank": "Location must be at least 3 characters.", "min_length": "Location must be at least 3 characters.", "max_length": "Location must be at most 100 characters.", }, @@ -30,7 +31,7 @@ class EventSerializer(serializers.ModelSerializer): max_length=100, min_length=3, error_messages={ - "required": "Host must be at least 3 characters.", + "blank": "Host must be at least 3 characters.", "min_length": "Host must be at least 3 characters.", "max_length": "Host must be at most 100 characters.", }, @@ -39,6 +40,7 @@ class EventSerializer(serializers.ModelSerializer): class Meta: model = Event fields = [ + "id", "start", "end", "name", @@ -50,7 +52,7 @@ class Meta: "from_mail", "ordering", ] - read_only_fields = ["owner", "created_at", "updated_at"] + read_only_fields = ["id", "owner", "created_at", "updated_at"] class EventView(APIView): @@ -92,11 +94,17 @@ def post(self, request) -> Response: class EventDetailView(APIView): def get(self, request, event_id) -> Response: # Logic to get details of an event - pass + event = get_object_or_404(Event, id=event_id) + serializer = EventSerializer(event) + return Response(serializer.data, status=status.HTTP_200_OK) def put(self, request, event_id) -> Response: # Logic to update details of an event - pass + event = get_object_or_404(Event, id=event_id) + serializer = EventSerializer(event, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) def delete(self, request, event_id) -> Response: # Logic to delete an event From 84e776df1b575873872af20f139a00c24a511efd Mon Sep 17 00:00:00 2001 From: Chloe Chen <133549470+celloii@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:15:19 -0400 Subject: [PATCH 08/14] DEV-650 Get User Events (#27) * added get handler in user_events_view * add user/events/ as a path * imported EventSerializer directly to use it * fixed changes and ran ruff --- backend/config/urls.py | 2 ++ backend/hoagiecalendar/api/user_events_view.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 backend/hoagiecalendar/api/user_events_view.py diff --git a/backend/config/urls.py b/backend/config/urls.py index c0a22b4..09c71db 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -19,9 +19,11 @@ from django.urls import path from hoagiecalendar.api.event_views import EventDetailView, EventView +from hoagiecalendar.api.user_events_view import UserEventsView urlpatterns = [ path("admin/", admin.site.urls), path("event/", EventView.as_view(), name="event"), path("event//", EventDetailView.as_view(), name="event-detail"), + path("user/events/", UserEventsView.as_view(), name="user-events"), ] diff --git a/backend/hoagiecalendar/api/user_events_view.py b/backend/hoagiecalendar/api/user_events_view.py new file mode 100644 index 0000000..f6adabd --- /dev/null +++ b/backend/hoagiecalendar/api/user_events_view.py @@ -0,0 +1,14 @@ +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView + +from ..models.event import Event +from .event_views import EventSerializer + + +class UserEventsView(APIView): + # Logic to get events user created + def get(self, request) -> Response: + events = Event.objects.filter(owner=request.user) + serializer = EventSerializer(events, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) From 5a5e3fa3f3814bc211899934372a2ce60b6662c0 Mon Sep 17 00:00:00 2001 From: Chloe Chen <133549470+celloii@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:00:54 -0400 Subject: [PATCH 09/14] DEV-653 Week View (#28) * week view that the events will go on * gray to left side and heading, more rounded edges, colors from the theme * darker border * replaced theme colors with import * removed redundant line --- frontend/components/weekView/weekView.tsx | 253 ++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 frontend/components/weekView/weekView.tsx diff --git a/frontend/components/weekView/weekView.tsx b/frontend/components/weekView/weekView.tsx new file mode 100644 index 0000000..6969d31 --- /dev/null +++ b/frontend/components/weekView/weekView.tsx @@ -0,0 +1,253 @@ +'use client'; + +import { useEffect, useRef } from 'react'; + +import { useTheme } from 'evergreen-ui'; + +const HOUR_HEIGHT = 60; +const TIME_COL_WIDTH = 56; +const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + +function getWeekDates(date: Date): Date[] { + const sunday = new Date(date); + sunday.setDate(date.getDate() - date.getDay()); + sunday.setHours(0, 0, 0, 0); + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(sunday); + d.setDate(sunday.getDate() + i); + return d; + }); +} + +function formatHour(hour: number): string { + if (hour === 0) return '12 AM'; + if (hour < 12) return `${hour} AM`; + if (hour === 12) return '12 PM'; + return `${hour - 12} PM`; +} + +export default function WeekView() { + const { colors } = useTheme(); + const scrollRef = useRef(null); + const now = new Date(); + const weekDates = getWeekDates(now); + const todayIndex = now.getDay(); + const currentMinuteOffset = (now.getHours() * 60 + now.getMinutes()) * (HOUR_HEIGHT / 60); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = Math.max(0, currentMinuteOffset - 200); + } + }, [currentMinuteOffset]); + + return ( +
+ {/* ── Day header row ── */} +
+ {/* Corner spacer */} +
+ + {weekDates.map((date, i) => { + const isToday = i === todayIndex; + const dayNum = date.getDate(); + return ( +
+ {/* Day name */} +
+ {DAYS[i]} +
+ + {/* Date circle */} +
+ {dayNum} +
+
+ ); + })} +
+ + {/* ── Scrollable grid body ── */} +
+
+ {/* ── Time labels column ── */} +
+ {Array.from({ length: 24 }, (_, hour) => ( +
+ {hour === 0 ? '' : formatHour(hour)} +
+ ))} +
+ + {/* ── Day columns ── */} +
+ {weekDates.map((_, colIdx) => ( +
+ {/* Alternating hour rows */} + {Array.from({ length: 24 }, (_, hour) => ( +
+ {/* Half-hour line */} +
+
+ ))} +
+ ))} + + {/* ── Current time indicator ── */} +
+
+
+
+
+
+
+
+ ); +} From e14995b5107a536fc8744b13700563961f43bd3a Mon Sep 17 00:00:00 2001 From: Karen Gao <48370412+karengao6@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:01:30 -0400 Subject: [PATCH 10/14] DEV-654 Month View (#32) * complete the get() handler for EventView * implement filter by multiple categories in EventView * implement multiple category filter in EventView * implement multiple category filter in EventView * implement multiple category filter in EventView * change gt/lt to gte/lte, got rid of ruff in dependencies list in pyproject.toml * remake uv lock * add .env.local file * testing monthView * created monthView * resolve merge conflicts * format prettier * use colors from useTheme, get rid of themeColors --- frontend/components/monthView/MonthView.tsx | 140 ++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 frontend/components/monthView/MonthView.tsx diff --git a/frontend/components/monthView/MonthView.tsx b/frontend/components/monthView/MonthView.tsx new file mode 100644 index 0000000..5888e2f --- /dev/null +++ b/frontend/components/monthView/MonthView.tsx @@ -0,0 +1,140 @@ +'use client'; + +import { useTheme } from 'evergreen-ui'; + +const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + +function getMonthMatrix(year: number, month: number): (Date | null)[][] { + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const cells: (Date | null)[] = [ + ...Array(firstDay).fill(null), + ...Array.from({ length: daysInMonth }, (_, i) => new Date(year, month, i + 1)), + ]; + // Pad to full weeks + while (cells.length % 7 !== 0) cells.push(null); + const weeks: (Date | null)[][] = []; + for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7)); + return weeks; +} + +interface MonthViewProps { + month?: number; // 0-indexed + year?: number; +} + +export default function MonthView({ month, year }: MonthViewProps) { + const { colors } = useTheme(); + + const now = new Date(); + const displayMonth = month !== undefined ? month : now.getMonth(); + const displayYear = year !== undefined ? year : now.getFullYear(); + + const weeks = getMonthMatrix(displayYear, displayMonth); + + return ( +
+ {/* ── Day name header row ── */} +
+ {DAYS.map((day) => ( +
+ {day} +
+ ))} +
+ + {/* ── Calendar rows ── */} +
+ {weeks.map((week, wi) => ( +
+ {week.map((date, di) => { + const isToday = + date !== null && + date.getDate() === now.getDate() && + date.getMonth() === now.getMonth() && + date.getFullYear() === now.getFullYear(); + + return ( +
+ {date !== null && ( +
+ {date.getDate()} +
+ )} +
+ ); + })} +
+ ))} +
+
+ ); +} From 99707d88f823bad5e9c63a3ee75ebbf6a64a5655 Mon Sep 17 00:00:00 2001 From: Chloe Chen <133549470+celloii@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:02:06 -0400 Subject: [PATCH 11/14] DEV-719 Event Tile on Month View (#36) * added event placeholders for monthly view * fixed ESlint warnings * fixed ESlint warnings --- frontend/app/month-view-test/page.tsx | 9 + frontend/components/monthView/MonthView.tsx | 121 ++++- frontend/lib/hoagie-ui/useEvents.ts | 522 ++++++++++++++++++++ frontend/types.ts | 14 + 4 files changed, 646 insertions(+), 20 deletions(-) create mode 100644 frontend/app/month-view-test/page.tsx create mode 100644 frontend/lib/hoagie-ui/useEvents.ts diff --git a/frontend/app/month-view-test/page.tsx b/frontend/app/month-view-test/page.tsx new file mode 100644 index 0000000..5f67df8 --- /dev/null +++ b/frontend/app/month-view-test/page.tsx @@ -0,0 +1,9 @@ +import MonthView from '@/components/monthView/MonthView'; + +export default function MonthViewTestPage() { + return ( +
+ +
+ ); +} diff --git a/frontend/components/monthView/MonthView.tsx b/frontend/components/monthView/MonthView.tsx index 5888e2f..15f4c73 100644 --- a/frontend/components/monthView/MonthView.tsx +++ b/frontend/components/monthView/MonthView.tsx @@ -2,7 +2,10 @@ import { useTheme } from 'evergreen-ui'; +import { useEvents, getCategoryColor } from '@/lib/hoagie-ui/useEvents'; + const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; +const MAX_VISIBLE_EVENTS = 3; function getMonthMatrix(year: number, month: number): (Date | null)[][] { const firstDay = new Date(year, month, 1).getDay(); @@ -11,13 +14,29 @@ function getMonthMatrix(year: number, month: number): (Date | null)[][] { ...Array(firstDay).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => new Date(year, month, i + 1)), ]; - // Pad to full weeks while (cells.length % 7 !== 0) cells.push(null); const weeks: (Date | null)[][] = []; for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7)); return weeks; } +function isSameDay(a: Date, b: Date): boolean { + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ); +} + +function formatEventTime(iso: string): string { + const d = new Date(iso); + const h = d.getHours(); + const min = d.getMinutes(); + const suffix = h >= 12 ? 'p' : 'a'; + const hour = h % 12 === 0 ? 12 : h % 12; + return min === 0 ? `${hour}${suffix}` : `${hour}:${String(min).padStart(2, '0')}${suffix}`; +} + interface MonthViewProps { month?: number; // 0-indexed year?: number; @@ -32,6 +51,10 @@ export default function MonthView({ month, year }: MonthViewProps) { const weeks = getMonthMatrix(displayYear, displayMonth); + const startTime = new Date(displayYear, displayMonth, 1); + const endTime = new Date(displayYear, displayMonth + 1, 0, 23, 59, 59); + const { events } = useEvents(startTime, endTime); + return (
isSameDay(new Date(e.start), date)) + : []; + const visibleEvents = dayEvents.slice(0, MAX_VISIBLE_EVENTS); + const overflow = dayEvents.length - MAX_VISIBLE_EVENTS; + return (
{date !== null && ( -
- {date.getDate()} -
+ <> + {/* Date number */} +
+ {date.getDate()} +
+ + {/* Event pills */} +
+ {visibleEvents.map((event) => { + const color = getCategoryColor(event.category); + return ( +
+ {formatEventTime(event.start)}{' '} + {event.name} +
+ ); + })} + + {overflow > 0 && ( +
+ +{overflow} more +
+ )} +
+ )}
); diff --git a/frontend/lib/hoagie-ui/useEvents.ts b/frontend/lib/hoagie-ui/useEvents.ts new file mode 100644 index 0000000..8b1d7eb --- /dev/null +++ b/frontend/lib/hoagie-ui/useEvents.ts @@ -0,0 +1,522 @@ +/** + * @overview Hook for fetching calendar events. + * + * Copyright © 2021-2026 Hoagie Club and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree or at https://github.com/HoagieClub/calendar/blob/main/LICENSE. + * + * Permission is granted under the MIT License to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the software. This software is provided "as-is", without warranty of any kind. + */ + +import { useMemo } from 'react'; + +import type { CalendarEvent } from '@/types'; + +const y = new Date().getFullYear(); +const m = new Date().getMonth(); + +const d = (day: number, hour: number, min = 0) => new Date(y, m, day, hour, min).toISOString(); + +// Placeholder events for development — replace with real API call when ready +const PLACEHOLDER_EVENTS: CalendarEvent[] = [ + { + id: '1', + name: 'Campus Pickup Basketball', + start: d(1, 10), + end: d(1, 11), + location: 'Dillon Gym', + description: '', + host: 'Athletics', + owner: '', + category: 'sports', + from_mail: false, + ordering: 0, + }, + { + id: '2', + name: 'Housing Info Session', + start: d(1, 14), + end: d(1, 15), + location: 'Rocky Common Room', + description: '', + host: 'Housing Office', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '3', + name: 'Free Bagels', + start: d(2, 10), + end: d(2, 11), + location: 'CS Building', + description: '', + host: 'CS Dept', + owner: '', + category: 'food', + from_mail: false, + ordering: 0, + }, + { + id: '4', + name: 'Dance Team Practice', + start: d(2, 16), + end: d(2, 18), + location: 'Dillon Dance Studio', + description: '', + host: 'Dance Team', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '5', + name: 'Film Club Screening', + start: d(3, 17), + end: d(3, 20), + location: 'McCosh 50', + description: '', + host: 'Film Club', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '6', + name: 'CS Department Seminar', + start: d(3, 19), + end: d(3, 20), + location: 'Friend 101', + description: '', + host: 'CS Dept', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '7', + name: 'Sunday Movie Night', + start: d(5, 19), + end: d(5, 22), + location: 'Whitman Common Room', + description: '', + host: 'Whitman', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '8', + name: 'Quantum Computing Seminar', + start: d(6, 10), + end: d(6, 11, 30), + location: 'Friend 101', + description: '', + host: 'CS Dept', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '9', + name: 'Pickup Basketball', + start: d(6, 16), + end: d(6, 17), + location: 'Dillon Gym', + description: '', + host: 'Athletics', + owner: '', + category: 'sports', + from_mail: false, + ordering: 0, + }, + { + id: '10', + name: 'Study Hall', + start: d(6, 18), + end: d(6, 20), + location: 'Firestone Library', + description: '', + host: 'Library', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '11', + name: 'Acapella Rehearsal', + start: d(6, 20), + end: d(6, 21), + location: 'Richardson Auditorium', + description: '', + host: 'Acapella', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '12', + name: 'Mock Interview Night', + start: d(6, 21), + end: d(6, 22), + location: 'Frist Campus Center', + description: '', + host: 'Career Services', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '13', + name: 'CS Workshop', + start: d(7, 9), + end: d(7, 11), + location: 'Friend 008', + description: '', + host: 'CS Dept', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '14', + name: 'Free Pizza: CS Dept', + start: d(8, 12), + end: d(8, 13), + location: 'CS Building Lobby', + description: '', + host: 'CS Dept', + owner: '', + category: 'food', + from_mail: false, + ordering: 0, + }, + { + id: '15', + name: 'Trivia Night', + start: d(8, 15), + end: d(8, 17), + location: 'Frist Campus Center', + description: '', + host: 'Student Gov', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '16', + name: 'Mock Interview', + start: d(8, 17), + end: d(8, 18), + location: 'Career Services', + description: '', + host: 'Career Services', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '17', + name: 'Networking Dinner', + start: d(8, 19), + end: d(8, 21), + location: 'Prospect House', + description: '', + host: 'Alumni Office', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '18', + name: 'Volleyball Practice', + start: d(9, 16), + end: d(9, 18), + location: 'Dillon Gym', + description: '', + host: 'Athletics', + owner: '', + category: 'sports', + from_mail: false, + ordering: 0, + }, + { + id: '19', + name: 'Hack Princeton', + start: d(10, 18), + end: d(11, 18), + location: 'Friend Center', + description: '', + host: 'HackPrinceton', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '20', + name: 'Acapella Concert', + start: d(12, 19), + end: d(12, 21), + location: 'Richardson Auditorium', + description: '', + host: 'Acapella Groups', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '21', + name: 'Career Fair', + start: d(13, 11), + end: d(13, 16), + location: 'Jadwin Gym', + description: '', + host: 'Career Services', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '22', + name: 'Free Lunch', + start: d(13, 13), + end: d(13, 14), + location: 'Frist Food Court', + description: '', + host: 'Student Gov', + owner: '', + category: 'food', + from_mail: false, + ordering: 0, + }, + { + id: '23', + name: 'Chess Club', + start: d(13, 17), + end: d(13, 19), + location: 'Marx Hall', + description: '', + host: 'Chess Club', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '24', + name: 'Investment Banking Info', + start: d(13, 18), + end: d(13, 19), + location: 'Robertson Hall', + description: '', + host: 'Finance Club', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '25', + name: 'Robotics Showcase', + start: d(13, 19), + end: d(13, 21), + location: 'Engineering Quad', + description: '', + host: 'Robotics Club', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '26', + name: 'Thesis Chapter Due', + start: d(14, 23, 59), + end: d(14, 23, 59), + location: 'Online', + description: '', + host: 'Academic Affairs', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '27', + name: 'Free Pizza', + start: d(15, 12), + end: d(15, 13), + location: 'CS Building', + description: '', + host: 'CS Dept', + owner: '', + category: 'food', + from_mail: false, + ordering: 0, + }, + { + id: '28', + name: 'Fringe Festival Preview', + start: d(15, 17), + end: d(15, 19), + location: 'Lewis Center', + description: '', + host: 'Arts Council', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '29', + name: 'BSU Weekly Meeting', + start: d(new Date().getDate(), 10), + end: d(new Date().getDate(), 11), + location: '3rd floor Frist', + description: '', + host: 'BSU', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '30', + name: 'Mental Health Workshop', + start: d(new Date().getDate(), 16), + end: d(new Date().getDate(), 17), + location: 'McCosh Health Center', + description: '', + host: 'UHS', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '31', + name: 'Mock Interview Night', + start: d(new Date().getDate(), 18), + end: d(new Date().getDate(), 19), + location: 'Career Services', + description: '', + host: 'Career Services', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '32', + name: 'African Students Assoc.', + start: d(new Date().getDate(), 17), + end: d(new Date().getDate(), 18), + location: 'Carl Icahn Lab', + description: '', + host: 'PASA', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '33', + name: 'Debate Club', + start: d(new Date().getDate(), 19), + end: d(new Date().getDate(), 20), + location: 'Robertson Hall', + description: '', + host: 'Debate Club', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '34', + name: 'Housing Application Deadline', + start: d(25, 23, 59), + end: d(25, 23, 59), + location: 'Online', + description: '', + host: 'Housing Office', + owner: '', + category: 'academic', + from_mail: false, + ordering: 0, + }, + { + id: '35', + name: 'Senior Thesis Concert', + start: d(26, 19), + end: d(26, 21), + location: 'Richardson Auditorium', + description: '', + host: 'Music Dept', + owner: '', + category: 'social', + from_mail: false, + ordering: 0, + }, + { + id: '36', + name: 'Late Night Pancakes', + start: d(28, 23), + end: d(29, 0), + location: 'Frist Campus Center', + description: '', + host: 'Student Gov', + owner: '', + category: 'food', + from_mail: false, + ordering: 0, + }, +]; + +export const CATEGORY_COLORS: Record = { + sports: { bg: '#bfdbfe', text: '#0C447C' }, + academic: { bg: '#ddd6fe', text: '#3C3489' }, + food: { bg: '#fed7aa', text: '#633806' }, + social: { bg: '#bbf7d0', text: '#27500A' }, + default: { bg: '#e5e7eb', text: '#374151' }, +}; + +export function getCategoryColor(category: string): { bg: string; text: string } { + return CATEGORY_COLORS[category] ?? CATEGORY_COLORS.default; +} + +type UseEventsResult = { + events: CalendarEvent[]; + isLoading: boolean; + error: string | null; +}; + +export function useEvents(startTime: Date, endTime: Date): UseEventsResult { + const events = useMemo( + () => + PLACEHOLDER_EVENTS.filter( + (e) => new Date(e.start) <= endTime && new Date(e.end) >= startTime + ), + [startTime, endTime] + ); + + return { events, isLoading: false, error: null }; +} diff --git a/frontend/types.ts b/frontend/types.ts index 173bbb2..cb45e57 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -22,3 +22,17 @@ export type HoagieUser = { name?: string; email?: string; }; + +export type CalendarEvent = { + id: string; + start: string; // ISO datetime string + end: string; // ISO datetime string + name: string; + location: string; + description: string; + host: string; + owner: string; + category: string; + from_mail: boolean; + ordering: number; +}; From 51fc61aa5a10d0d181171e0919382aeb6d41399a Mon Sep 17 00:00:00 2001 From: ZhaoSongZh7 <104543015+ZhaoSongZh7@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:54:25 -0400 Subject: [PATCH 12/14] DEV-652 Day View (#30) * Added Dayview visualizer * Added hover animation and dialog popup * prettier fixes * reverted to no onSelect * prettier fixes * Clean up event card * Remove temp day view page --------- Co-authored-by: Jenny Fan <40123244+jfmath04@users.noreply.github.com> Co-authored-by: zz3188 --- frontend/app/month-view-test/page.tsx | 9 - frontend/components/DayView.tsx | 335 ++++++++++++++++++++++++++ frontend/components/Event.tsx | 220 +++++++++++++++++ 3 files changed, 555 insertions(+), 9 deletions(-) delete mode 100644 frontend/app/month-view-test/page.tsx create mode 100644 frontend/components/DayView.tsx create mode 100644 frontend/components/Event.tsx diff --git a/frontend/app/month-view-test/page.tsx b/frontend/app/month-view-test/page.tsx deleted file mode 100644 index 5f67df8..0000000 --- a/frontend/app/month-view-test/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import MonthView from '@/components/monthView/MonthView'; - -export default function MonthViewTestPage() { - return ( -
- -
- ); -} diff --git a/frontend/components/DayView.tsx b/frontend/components/DayView.tsx new file mode 100644 index 0000000..225c959 --- /dev/null +++ b/frontend/components/DayView.tsx @@ -0,0 +1,335 @@ +'use client'; + +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { Pane, Text, majorScale } from 'evergreen-ui'; + +import Event, { type CalendarEvent } from '@/components/Event'; + +const HOUR_HEIGHT = 88; +const TIME_LABEL_WIDTH = 72; +const INITIAL_SCROLL_PADDING = 160; +const TOUCHING_EVENT_GAP = 4; +const EVENT_LANES = 5; + +const SAMPLE_EVENTS: CalendarEvent[] = [ + { + id: 1, + name: 'Dissertation Writing Workshop', + startHour: 9, + endHour: 20, + location: 'Graduate College Common Room', + host: 'Graduate School', + attendees: 22, + category: 'Other', + }, + { + id: 2, + name: 'Annual Princeton Arts Invitational Showcase', + startHour: 12, + endHour: 20, + location: '185 Nassau Street Gallery', + host: 'Princeton Arts Council', + attendees: 73, + category: 'Arts & Culture', + }, + { + id: 3, + name: 'Inclusive Guest Lecture: Dr. Priya Mehta', + startHour: 12, + endHour: 13.5, + location: 'Robertson Hall 016', + host: 'Woodrow Wilson School', + attendees: 47, + category: 'Academic', + }, + { + id: 4, + name: 'Preview Day Farmers Market Lunch Giveaway', + startHour: 12, + endHour: 13, + location: 'Frist South Lawn', + host: 'Campus Dining', + attendees: 15, + category: 'Free Food', + }, + { + id: 5, + name: 'Quantum Computing Seminar', + startHour: 15, + endHour: 16.5, + location: 'Jadwin Hall A10', + host: 'Physics Department', + attendees: 17, + category: 'Academic', + }, + { + id: 6, + name: 'Model UN Committee Session', + startHour: 16.5, + endHour: 18.5, + location: 'Whig Hall Senate Chamber', + host: 'Princeton Model UN', + attendees: 21, + category: 'Career', + }, + { + id: 7, + name: 'Debate Club Practice', + startHour: 16.5, + endHour: 18, + location: '1879 Hall', + host: 'Debate Panel', + attendees: 18, + category: 'Other', + }, + { + id: 8, + name: 'Campus YMCA Yoga', + startHour: 16.5, + endHour: 17.5, + location: 'Dillon Gym', + host: 'Campus YMCA', + attendees: 45, + category: 'Sports & Fitness', + }, + { + id: 9, + name: 'Graduate Student Happy Hour', + startHour: 17, + endHour: 18, + location: 'Graduate College', + host: 'Grad School', + attendees: 53, + category: 'Social Events', + }, + { + id: 10, + name: 'Princeton International Friends Mixer', + startHour: 18, + endHour: 20, + location: 'Friend Center', + host: 'International Students Association', + attendees: 52, + category: 'Social Events', + }, + { + id: 11, + name: 'African Students Assoc Meetup', + startHour: 17.5, + endHour: 19, + location: 'Third World Center', + host: 'Princeton African Students Association', + attendees: 29, + category: 'Social Events', + }, + { + id: 12, + name: 'Take-Back the Night Rally', + startHour: 19, + endHour: 21, + location: 'Cannon Green', + host: 'Women Center', + attendees: 87, + category: 'Housing & Sales', + }, + { + id: 13, + name: 'Tiger Investments General Meeting', + startHour: 19, + endHour: 20.5, + location: 'Bendheim Hall 103', + host: 'Tiger Investments', + attendees: 24, + category: 'Career', + }, + { + id: 14, + name: 'USG Town Hall: Campus Dining', + startHour: 19, + endHour: 20.5, + location: 'Whig Hall Senate Chamber', + host: 'Undergraduate Student Government', + attendees: 24, + category: 'Other', + }, + { + id: 15, + name: 'dilisa Dance Company Workshop', + startHour: 20, + endHour: 21.5, + location: 'New South Dance Studio', + host: 'dilisa Dance Company', + attendees: 31, + category: 'Arts & Culture', + }, +]; + +function formatHour(hour: number): string { + const period = hour < 12 ? 'AM' : 'PM'; + const normalizedHour = hour % 12 === 0 ? 12 : hour % 12; + return `${normalizedHour} ${period}`; +} + +export function DayView() { + const scrollRef = useRef(null); + const hasAutoScrolledRef = useRef(false); + const [now, setNow] = useState(() => new Date()); + const totalHeight = HOUR_HEIGHT * 24; + const currentTimePosition = useMemo( + () => (now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600) * HOUR_HEIGHT, + [now] + ); + + useEffect(() => { + const timer = window.setInterval(() => { + setNow(new Date()); + }, 60000); + + return () => window.clearInterval(timer); + }, []); + + useEffect(() => { + const container = scrollRef.current; + if (!container || hasAutoScrolledRef.current) { + return; + } + + const targetScroll = Math.max( + 0, + currentTimePosition - container.clientHeight / 2 + INITIAL_SCROLL_PADDING + ); + container.scrollTop = targetScroll; + hasAutoScrolledRef.current = true; + }, [currentTimePosition]); + + const hours = Array.from({ length: 24 }, (_, hour) => hour); + + return ( + + + + + + {hours.map((hour) => ( + + + {formatHour(hour)} + + + + ))} + + + {SAMPLE_EVENTS.map((event, index) => { + const duration = event.endHour - event.startHour; + const top = event.startHour * HOUR_HEIGHT; + const height = Math.max(duration * HOUR_HEIGHT, 56); + const lane = index % EVENT_LANES; + const laneWidth = `calc(${100 / EVENT_LANES}% - 10px)`; + const halfGap = TOUCHING_EVENT_GAP / 2; + const hasEventEndingAtStart = SAMPLE_EVENTS.some( + (otherEvent, otherIndex) => + otherEvent.id !== event.id && + otherIndex % EVENT_LANES === lane && + otherEvent.endHour === event.startHour + ); + const hasEventStartingAtEnd = SAMPLE_EVENTS.some( + (otherEvent, otherIndex) => + otherEvent.id !== event.id && + otherIndex % EVENT_LANES === lane && + otherEvent.startHour === event.endHour + ); + const topGap = hasEventEndingAtStart ? halfGap : 0; + const bottomGap = hasEventStartingAtEnd ? halfGap : 0; + + return ( + + ); + })} + + + + + + + + + ); +} + +export default DayView; diff --git a/frontend/components/Event.tsx b/frontend/components/Event.tsx new file mode 100644 index 0000000..2471d0c --- /dev/null +++ b/frontend/components/Event.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useState } from 'react'; + +import { Heading, Pane, Text } from 'evergreen-ui'; + +export const EVENT_CATEGORIES = [ + 'Social Events', + 'Academic', + 'Free Food', + 'Arts & Culture', + 'Sports & Fitness', + 'Career', + 'Housing & Sales', + 'Other', +] as const; + +export type EventCategory = (typeof EVENT_CATEGORIES)[number]; + +const CATEGORY_STYLES: Record< + EventCategory, + { background: string; border: string; color: string; label: string } +> = { + 'Social Events': { + background: '#fff3f1', + border: '#ef6b61', + color: '#c2473d', + label: 'SOCIAL EVENTS', + }, + Academic: { + background: '#f2f7ff', + border: '#5b87ff', + color: '#446fd9', + label: 'ACADEMIC', + }, + 'Free Food': { + background: '#fff6eb', + border: '#ef9b3d', + color: '#cf7a1f', + label: 'FREE FOOD', + }, + 'Arts & Culture': { + background: '#f7f0ff', + border: '#9b6cff', + color: '#7d55da', + label: 'ARTS & CULTURE', + }, + 'Sports & Fitness': { + background: '#edf9f1', + border: '#66c78d', + color: '#439965', + label: 'SPORTS & FITNESS', + }, + Career: { + background: '#f3f0ff', + border: '#7266f0', + color: '#5b50cc', + label: 'CAREER', + }, + 'Housing & Sales': { + background: '#fff1f3', + border: '#eb5f78', + color: '#c94f64', + label: 'HOUSING & SALES', + }, + Other: { + background: '#f4f5f7', + border: '#9aa3b2', + color: '#5f6773', + label: 'OTHER', + }, +}; + +export type CalendarEvent = { + id: number; + name: string; + startHour: number; + endHour: number; + location: string; + host: string; + attendees: number; + category?: EventCategory; + description?: string; +}; + +type EventProps = { + event: CalendarEvent; + top: number; + left: string; + width: string; + height: number; +}; + +export function formatEventTime(startHour: number, endHour: number): string { + const toLabel = (value: number) => { + const hours = Math.floor(value); + const minutes = Math.round((value - hours) * 60); + const period = hours < 12 ? 'AM' : 'PM'; + const normalizedHour = hours % 12 === 0 ? 12 : hours % 12; + const minuteLabel = minutes === 0 ? '' : `:${String(minutes).padStart(2, '0')}`; + return `${normalizedHour}${minuteLabel} ${period}`; + }; + + return `${toLabel(startHour)} - ${toLabel(endHour)}`; +} + +export function Event({ event, top, left, width, height }: EventProps) { + const showExpandedDetails = height >= 120; + const showLocation = height >= 92; + const titleLineClamp = height >= 140 ? 2 : 1; + const categoryStyle = event.category + ? CATEGORY_STYLES[event.category] + : CATEGORY_STYLES['Other']; + const [isHighlighted, setIsHighlighted] = useState(false); + + return ( + setIsHighlighted(true)} + onMouseLeave={() => setIsHighlighted(false)} + onFocus={() => setIsHighlighted(true)} + onBlur={() => setIsHighlighted(false)} + style={{ + transform: isHighlighted + ? 'translateY(-3px) scale(1.01)' + : 'translateY(0) scale(1)', + boxShadow: isHighlighted + ? '0 14px 30px rgba(100, 116, 139, 0.2)' + : '0 6px 18px rgba(100, 116, 139, 0.12)', + transition: 'transform 140ms ease, box-shadow 140ms ease', + }} + > + + + + + {formatEventTime(event.startHour, event.endHour)} + + + {`👥 ${event.attendees}`} + + + + {event.name} + + {showLocation ? ( + + {`📍 ${event.location}`} + + ) : null} + {showExpandedDetails ? ( + + + + {categoryStyle.label} + + + + {`via ${event.host}`} + + + ) : null} + + + ); +} + +export default Event; From 27f0f6a04b3531171e5128971bae0b5cf02503b6 Mon Sep 17 00:00:00 2001 From: ZhaoSongZh7 <104543015+ZhaoSongZh7@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:19:13 -0400 Subject: [PATCH 13/14] DEV-699: Add Team Members As Contributor (#34) * Added Dayview visualizer * Added hover animation and dialog popup * prettier fixes * reverted to no onSelect * prettier fixes * Updated team members page & changed linkedin link to imgur link * profile changes * Add past leads section, fix theme colors * Remove unnecessary changes * Remove day view files * Change titles to developer --------- Co-authored-by: Jenny Fan <40123244+jfmath04@users.noreply.github.com> Co-authored-by: zz3188 --- frontend/app/about/page.tsx | 291 +++++++++++++++++------- frontend/lib/hoagie-ui/Theme/themes.tsx | 2 +- 2 files changed, 214 insertions(+), 79 deletions(-) diff --git a/frontend/app/about/page.tsx b/frontend/app/about/page.tsx index 44ba481..97fc858 100644 --- a/frontend/app/about/page.tsx +++ b/frontend/app/about/page.tsx @@ -1,9 +1,8 @@ 'use client'; -import React from 'react'; +import React, { useState } from 'react'; import { Heading, majorScale, Pane, Text, useTheme } from 'evergreen-ui'; -import Image from 'next/image'; // --- Helper Components & Data --- interface member { @@ -12,20 +11,30 @@ interface member { imgSrc: string; socials: { linkedin: string; + github: string; }; } // Icon for social media links -const SocialIcon = ({ href, children }: { href: string; children: React.ReactNode }) => ( - - {children} - -); +const SocialIcon = ({ href, children }: { href: string; children: React.ReactNode }) => { + const theme = useTheme(); + const [hovered, setHovered] = useState(false); + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {children} + + ); +}; // SVG components for icons const LinkedinIcon = () => ( @@ -46,34 +55,205 @@ const LinkedinIcon = () => ( ); +const GitHubIcon = () => ( + + + +); + +const LeadCard = ({ lead }: { lead: member }) => { + const theme = useTheme(); + const [hovered, setHovered] = useState(false); + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + className='bg-white rounded-2xl shadow-lg overflow-hidden transform hover:scale-[1.02] transition-transform duration-300 ease-in-out' + > +
+ {/* Intentionally using img so direct Imgur URLs work without Next image domain config. */} + {/* eslint-disable-next-line @next/next/no-img-element */} + {lead.name} +
+

{lead.name}

+

+ {lead.role} +

+
+ + + + + + +
+
+
+
+ ); +}; + +const MemberCard = ({ member }: { member: member }) => { + const theme = useTheme(); + const [hovered, setHovered] = useState(false); + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + className='bg-white rounded-xl shadow-md p-6 text-center transform hover:-translate-y-2 transition-transform duration-300 ease-in-out' + > + {/* Intentionally using img so direct Imgur URLs work without Next image domain config. */} + {/* eslint-disable-next-line @next/next/no-img-element */} + {member.name} +

{member.name}

+

+ {member.role} +

+
+ + + + + + +
+
+ ); +}; + // Team data organized for easier management const teamLeads: member[] = [ { - name: 'Jenny Fan', + name: 'Zhao Song Zhou', role: 'Team Lead', - imgSrc: 'https://media.licdn.com/dms/image/v2/D4E03AQFOQEWyofLFhw/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1710648138236?e=1770249600&v=beta&t=YhbYphTxR6MEjk17qmvvw0dqbcdpbIwNrDs0oDU-BVM', + imgSrc: 'https://i.imgur.com/JeUh9dc.jpeg', + socials: { + linkedin: 'https://www.linkedin.com/in/zhao-song-zhou/', + github: 'https://github.com/ZhaoSongZh7', + }, + }, + { + name: 'Alvin Sze', + role: 'Team Lead', + imgSrc: 'https://i.imgur.com/mZy9kzp.jpeg', + socials: { + linkedin: 'https://www.linkedin.com/in/alvinsze/', + github: 'https://github.com/asze17', + }, + }, +]; + +const pastLeads: member[] = [ + { + name: 'Jenny Fan', + role: 'Team Lead (2025 - 2026)', + imgSrc: 'https://i.imgur.com/grwgWFZ.jpeg', socials: { linkedin: 'https://www.linkedin.com/in/jennyfan04/', + github: 'https://github.com/jfmath04', }, }, ]; const teamMembers = [ - // Add your name, role, and image here! { name: 'Chloe Lau', role: 'Product Manager', - imgSrc: 'https://media.licdn.com/dms/image/v2/D4E03AQFlLceS8QZODA/profile-displayphoto-crop_800_800/B4EZrciQfjKUAI-/0/1764636561365?e=1770854400&v=beta&t=qa6Xfoyb44M9dw9VHW_ofy9ChNiirP2hS6CMRYRWfto', + imgSrc: 'https://i.imgur.com/BVrnu64.jpeg', socials: { linkedin: 'https://www.linkedin.com/in/chloe-hc-lau/', + github: 'https://github.com/lauechlo', + }, + }, + { + name: 'Allison Lee', + role: 'Product Manager', + imgSrc: 'https://i.imgur.com/3SPqM7z.jpeg', + socials: { + linkedin: 'https://www.linkedin.com/in/allisonelee/', + github: 'https://github.com/allisonelee', + }, + }, + { + name: 'Helen Hui', + role: 'Product Manager', + imgSrc: 'https://i.imgur.com/kc4WIyv.jpeg', + socials: { + linkedin: 'https://www.linkedin.com/in/helen-hui-7125b929b/', + github: 'https://github.com/ilovehhhyn', + }, + }, + { + name: 'Erica Lee', + role: 'Developer', + imgSrc: 'https://i.imgur.com/1nXfGV4.png', + socials: { + linkedin: 'https://www.linkedin.com/in/ericayrlee/', + github: 'https://github.com/ericayrlee', }, }, { name: 'Angela Cai', - role: 'Software Engineer', - imgSrc: 'https://media.licdn.com/dms/image/v2/D4E03AQHpPTd04eUfhQ/profile-displayphoto-crop_800_800/B4EZrXdJAUHEAI-/0/1764551332672?e=1772668800&v=beta&t=LsG7vZaIRyXzf_3-7lrJiu2z_Hokd46Kvv5-3d2_Wpc', + role: 'Developer', + imgSrc: 'https://i.imgur.com/NASRtwq.jpeg', socials: { linkedin: 'https://www.linkedin.com/in/angcai/', + github: 'https://github.com/Ang-cai', + }, + }, + { + name: 'Karen Gao', + role: 'Developer', + imgSrc: 'https://i.imgur.com/dBnfnGt.jpeg', + socials: { + linkedin: 'https://www.linkedin.com/in/karen-a-gao/', + github: 'https://github.com/karengao6', + }, + }, + { + name: 'Chloe Chen', + role: 'Developer', + imgSrc: 'https://i.imgur.com/HoM0S1V.jpeg', + socials: { + linkedin: 'https://www.linkedin.com/in/chloe-chen-7388243b2/', + github: 'https://github.com/celloii', + }, + }, + { + name: 'Niv Levy', + role: 'Developer', + imgSrc: 'https://i.imgur.com/XbLGo6d.jpeg', + socials: { + linkedin: 'https://www.linkedin.com/in/niv-levy-012685258/', + github: 'https://github.com/NivLevy-gh', }, }, { @@ -128,47 +308,9 @@ export function App() {

Team Leadership

-
+
{teamLeads.map((lead) => ( -
-
- - {lead.name} - -
-

- {lead.name} -

-

- {lead.role} -

-
- - - -
-
-
-
+ ))}
@@ -180,26 +322,19 @@ export function App() {
{teamMembers.map((member) => ( -
- {member.name} -

{member.name}

-

{member.role}

-
- - - -
-
+ + ))} +
+ + + {/* Past Leads Section */} +
+

+ Past Leads +

+
+ {pastLeads.map((lead) => ( + ))}
diff --git a/frontend/lib/hoagie-ui/Theme/themes.tsx b/frontend/lib/hoagie-ui/Theme/themes.tsx index 91147b1..72e55c6 100644 --- a/frontend/lib/hoagie-ui/Theme/themes.tsx +++ b/frontend/lib/hoagie-ui/Theme/themes.tsx @@ -244,7 +244,7 @@ export const hoagieCalendar = mergeTheme(hoagieUI, { blue500: '#33BBBF', blue400: '#5CCED3', blue300: '#85DFE7', - blue200: '#ADCFEB', + blue200: '#a7dfe4', blue100: '#D6EFF2', blue50: '#E9FAFB', blue25: '#F4FDFD', From 6de54d78219e3a64aa247a656e4383b6a27b7707 Mon Sep 17 00:00:00 2001 From: celloii <133549470+celloii@users.noreply.github.com> Date: Fri, 24 Apr 2026 23:17:46 -0400 Subject: [PATCH 14/14] central page for the calendar where the day, week, and monthly views can be selected --- frontend/app/calendar/page.tsx | 5 + .../components/calendarPage/calendarPage.tsx | 323 ++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 frontend/app/calendar/page.tsx create mode 100644 frontend/components/calendarPage/calendarPage.tsx diff --git a/frontend/app/calendar/page.tsx b/frontend/app/calendar/page.tsx new file mode 100644 index 0000000..439cd5c --- /dev/null +++ b/frontend/app/calendar/page.tsx @@ -0,0 +1,5 @@ +import CalendarPage from '@/components/calendarPage/calendarPage'; + +export default function Page() { + return ; +} diff --git a/frontend/components/calendarPage/calendarPage.tsx b/frontend/components/calendarPage/calendarPage.tsx new file mode 100644 index 0000000..d70f4fc --- /dev/null +++ b/frontend/components/calendarPage/calendarPage.tsx @@ -0,0 +1,323 @@ +'use client'; + +import { useState } from 'react'; + +import { useTheme } from 'evergreen-ui'; + +import DayView from '@/components/DayView'; +import MonthView from '@/components/monthView/MonthView'; +import WeekView from '@/components/weekView/weekView'; + +type ViewType = 'Day' | 'Week' | 'Month'; + +function formatHeaderDate(view: ViewType, date: Date): string { + const opts: Intl.DateTimeFormatOptions = { month: 'long', year: 'numeric' }; + if (view === 'Month') { + return date.toLocaleString('en-US', opts); + } + if (view === 'Day') { + return date.toLocaleString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }); + } + // Week: show range + const sunday = new Date(date); + sunday.setDate(date.getDate() - date.getDay()); + const saturday = new Date(sunday); + saturday.setDate(sunday.getDate() + 6); + const startMonth = sunday.toLocaleString('en-US', { month: 'short' }); + const endMonth = saturday.toLocaleString('en-US', { month: 'short' }); + const year = saturday.getFullYear(); + if (startMonth === endMonth) { + return `${startMonth} ${sunday.getDate()} – ${saturday.getDate()}, ${year}`; + } + return `${startMonth} ${sunday.getDate()} – ${endMonth} ${saturday.getDate()}, ${year}`; +} + +function navigate(view: ViewType, date: Date, direction: 1 | -1): Date { + const next = new Date(date); + if (view === 'Day') next.setDate(date.getDate() + direction); + else if (view === 'Week') next.setDate(date.getDate() + direction * 7); + else next.setMonth(date.getMonth() + direction); + return next; +} + +// Icons as simple SVG components +function SearchIcon() { + return ( + + + + + ); +} + +function HelpIcon() { + return ( + + + + + + ); +} + +function SettingsIcon() { + return ( + + + + + ); +} + +// function MenuIcon() { +// return ( +// +// +// +// +// +// ); +// } + +function ChevronLeft() { + return ( + + + + ); +} + +function ChevronRight() { + return ( + + + + ); +} + +export default function CalendarPage() { + const { colors } = useTheme(); + const themeColors = colors as unknown as Record; + const TEAL = themeColors['hoagie-teal'] ?? '#1EA7AE'; + + const [view, setView] = useState('Week'); + const [currentDate, setCurrentDate] = useState(new Date()); + + const goToToday = () => setCurrentDate(new Date()); + const goBack = () => setCurrentDate(navigate(view, currentDate, -1)); + const goForward = () => setCurrentDate(navigate(view, currentDate, 1)); + + const iconButtonStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 34, + height: 34, + borderRadius: 8, + border: 'none', + background: 'transparent', + color: colors.gray700, + cursor: 'pointer', + transition: 'background 0.15s', + // eslint-disable-next-line no-undef + } as React.CSSProperties; + + return ( +
+ {/* ── Teal accent bar ── */} +
+ + {/* ── Top navbar ── */} +
+ {/* Menu + Logo */} + {/* + +
+ + hoagie + + + calendar + + + BETA + +
*/} + + {/* Navigation: back, Today, forward, date label */} + + + + + + + + {formatHeaderDate(view, currentDate)} + + + {/* Spacer */} +
+ + {/* Right side: icon buttons + view switcher */} + + + + + {/* View switcher */} +
+ {(['Day', 'Week', 'Month'] as ViewType[]).map((v) => ( + + ))} +
+
+ + {/* ── Main content ── */} +
+ {view === 'Day' && ( +
+ +
+ )} + {view === 'Week' && ( +
+ +
+ )} + {view === 'Month' && ( +
+ +
+ )} +
+
+ ); +}